first commit

This commit is contained in:
Alfiansyahp2 2025-06-25 08:16:16 +07:00
parent e8c1e4bbad
commit c2c79c2a13
137 changed files with 18368 additions and 186948 deletions

View File

@ -1,58 +0,0 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
LOG_CHANNEL=stack
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=
BROADCAST_DRIVER=log
CACHE_DRIVER=file
FILESYSTEM_DISK=local
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120
MEMCACHED_HOST=127.0.0.1
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=smtp
MAIL_HOST=mailpit
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_HOST=
PUSHER_PORT=443
PUSHER_SCHEME=https
PUSHER_APP_CLUSTER=mt1
VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
VITE_PUSHER_HOST="${PUSHER_HOST}"
VITE_PUSHER_PORT="${PUSHER_PORT}"
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

View File

@ -9,6 +9,7 @@
use App\Models\Role;
use App\Models\Makanan;
use App\Models\Rekomendasi;
use App\Models\WaktuMakan;
use App\Http\Requests\UserRequest;
use App\Http\Requests\RoleRequest;
use App\Http\Requests\UpdateUserRequest;
@ -21,24 +22,39 @@ class AdminController extends Controller
{
//
public function admindash()
{
$userCount = User::count();
$makananCount = Makanan::count();
{
$userCount = User::whereHas('role', function($query) {
$query->where('name', 'user');
})->count();
$makananCount = Makanan::count();
$waktuMakanCount = DB::table('waktu_makans')->count();
$komponenCount = DB::table('komponens')->count();
// Ambil 5 rekomendasi tertinggi dengan relasi makanan
$chartData = Rekomendasi::with('makanan')
->orderByDesc('nilai_akhir')
->take(5)
->get()
->map(function ($item) {
return [
'name' => $item->makanan->nama,
'value' => $item->nilai_akhir
];
});
// Ambil data waktu makan dengan eager loading
$waktuMakans = WaktuMakan::with(['rekomendasis' => function($query) {
$query->orderBy('tanggal_rekomendasi', 'desc');
}, 'komponens'])
->get()
->map(function($waktuMakan) {
$waktuMakan->has_recommendation = $waktuMakan->rekomendasis->isNotEmpty();
$waktuMakan->latest_calculation = $waktuMakan->rekomendasis->first()?->tanggal_rekomendasi;
return $waktuMakan;
});
return view('admin.admindash', compact('userCount', 'makananCount', 'chartData'));
}
// Ambil 5 rekomendasi tertinggi dengan relasi makanan
$chartData = Rekomendasi::with('makanan')
->orderByDesc('nilai_akhir')
->take(5)
->get()
->map(function ($item) {
return [
'name' => $item->makanan->nama,
'value' => $item->nilai_akhir
];
});
return view('admin.admindash', compact('userCount', 'makananCount', 'chartData', 'waktuMakanCount', 'komponenCount', 'waktuMakans'));
}
// USER

View File

@ -0,0 +1,137 @@
<?php
namespace App\Http\Controllers;
use App\Models\UserAlertSetting;
use App\Models\WaktuMakan;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Carbon\Carbon;
class AlertSettingController extends Controller
{
public function index()
{
$user = Auth::user();
$alertSetting = UserAlertSetting::where('user_id', $user->id)->first();
if (!$alertSetting) {
$alertSetting = UserAlertSetting::create([
'user_id' => $user->id,
'is_enabled' => true,
'enabled_waktu_makan_ids' => WaktuMakan::where('is_active', true)->pluck('id')->toArray()
]);
}
$waktuMakans = WaktuMakan::where('is_active', true)->get();
return view('user.alert-settings', compact('alertSetting', 'waktuMakans'));
}
public function update(Request $request)
{
$user = Auth::user();
$alertSetting = UserAlertSetting::where('user_id', $user->id)->first();
if (!$alertSetting) {
$alertSetting = new UserAlertSetting();
$alertSetting->user_id = $user->id;
}
$alertSetting->is_enabled = $request->has('is_enabled');
$alertSetting->enabled_waktu_makan_ids = $request->enabled_waktu_makan_ids ?? [];
$alertSetting->save();
return redirect()->back()->with('success', 'Pengaturan alert berhasil diperbarui!');
}
public function toggle()
{
$user = Auth::user();
$alertSetting = UserAlertSetting::where('user_id', $user->id)->first();
if (!$alertSetting) {
$alertSetting = UserAlertSetting::create([
'user_id' => $user->id,
'is_enabled' => true,
'enabled_waktu_makan_ids' => WaktuMakan::where('is_active', true)->pluck('id')->toArray()
]);
}
$alertSetting->is_enabled = !$alertSetting->is_enabled;
$alertSetting->save();
return response()->json([
'success' => true,
'is_enabled' => $alertSetting->is_enabled,
'message' => $alertSetting->is_enabled ? 'Alert diaktifkan' : 'Alert dinonaktifkan'
]);
}
public function getCurrentAlerts()
{
$user = Auth::user();
$alertSetting = UserAlertSetting::where('user_id', $user->id)->first();
if (!$alertSetting || !$alertSetting->is_enabled) {
return response()->json(['alerts' => []]);
}
$currentTime = Carbon::now(config('app.timezone'));
$enabledWaktuMakan = $alertSetting->enabledWaktuMakan;
$alerts = [];
foreach ($enabledWaktuMakan as $waktuMakan) {
// Gunakan method baru untuk cek waktu makan
if ($waktuMakan->isWaktuMakanSekarang(30)) {
$alerts[] = [
'id' => $waktuMakan->id,
'nama' => $waktuMakan->nama,
'alert_text' => $waktuMakan->alert_text,
'waktu' => $waktuMakan->getWaktuFormatted(),
'waktu_ampm' => $waktuMakan->getWaktuAMPM(),
'timezone' => config('app.timezone')
];
}
}
return response()->json(['alerts' => $alerts]);
}
public function testAlert()
{
$user = Auth::user();
$alertSetting = UserAlertSetting::where('user_id', $user->id)->first();
if (!$alertSetting) {
$alertSetting = UserAlertSetting::create([
'user_id' => $user->id,
'is_enabled' => true,
'enabled_waktu_makan_ids' => WaktuMakan::where('is_active', true)->pluck('id')->toArray()
]);
}
$enabledWaktuMakan = $alertSetting->enabledWaktuMakan;
if ($enabledWaktuMakan->isEmpty()) {
return response()->json([
'success' => false,
'message' => 'Pilih minimal satu waktu makan untuk test alert!'
]);
}
// Ambil waktu makan pertama yang aktif
$testWaktuMakan = $enabledWaktuMakan->first();
return response()->json([
'success' => true,
'alert' => [
'nama' => $testWaktuMakan->nama,
'alert_text' => $testWaktuMakan->alert_text,
'waktu' => $testWaktuMakan->getWaktuFormatted(),
'waktu_ampm' => $testWaktuMakan->getWaktuAMPM(),
'timezone' => config('app.timezone')
]
]);
}
}

View File

@ -4,213 +4,516 @@
use Illuminate\Http\Request;
use App\Models\Kriteria;
use App\Models\Makanan; // alternatif
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
{
//
// 1. Tampilkan form pemilihan alternatif
use KriteriaTrait;
// 1. Tampilkan form pemilihan alternatif berdasarkan rekomendasi ahli
public function formPilihAlternatif(Request $request)
{
$kategoris = Kategori::all();
$sort = $request->query('sort'); // lemak | natrium | energi | karbohidrat
$order = $request->query('order'); // asc | desc
$search = $request->query('q'); // kata kunci nama makanan
$kategoriFilter = $request->kategori; // kategori
{
// Cek apakah tahap kriteria sudah selesai
if (!session('tahap_kriteria_selesai')) {
return redirect()->route('perbandingan')
->with('error', 'Selesaikan perhitungan bobot kriteria terlebih dahulu.');
}
$query = Makanan::query();
$komponens = Komponen::all();
$waktuMakans = WaktuMakan::all();
// Get filter parameters
$waktuMakanId = $request->waktu_makan_id;
$search = $request->q;
// 1. Search by name
if ($search) {
$query->where('nama', 'like', "%{$search}%");
// 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'
));
}
if ($kategoriFilter) {
$query->where('kategori_id', $kategoriFilter);
}
// 2. Sort
if (in_array($sort, ['lemak','natrium','energi','karbohidrat'])) {
$order = $order === 'asc' ? 'asc' : 'desc'; // default desc
$query->orderBy($sort, $order);
}
$makanans = $query->get();
return view('admin.alternatif.pilih', compact('makanans','sort','order','search', 'kategoriFilter', 'kategoris'));
}
// 2. Simpan pilihan alternatif ke session
// 2. Simpan pilihan alternatif dan hitung bobot
public function pilihAlternatif(Request $request)
{
$request->validate([
'alternatifs' => 'required|array|min:2',
]);
try {
$alternatifIds = $request->input('alternatifs');
$waktuMakanId = $request->input('waktu_makan_id');
$komponenIds = $request->input('komponen_ids');
$tanggalRekomendasi = now()->toDateString();
session(['alternatifs_dipilih' => $request->alternatifs]);
if (!$alternatifIds || !$waktuMakanId || !$komponenIds) {
return redirect()->back()
->with('error', 'Pilih minimal 4 alternatif untuk setiap komponen.');
}
return redirect()->route('alternatif.perbandingan');
// 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());
}
}
// 3. Tampilkan form perbandingan alternatif per kriteria
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()
{
$idAlternatif = session('alternatifs_dipilih');
try {
// Ambil data dari session
$alternatifsByKomponen = session('alternatifs_by_komponen');
if (!$idAlternatif || count($idAlternatif) < 2) {
return redirect()->route('alternatif.pilih')->with('error', 'Pilih minimal dua alternatif terlebih dahulu.');
if (!$alternatifsByKomponen) {
return redirect()->route('alternatif.view')
->with('error', 'Silakan lihat data alternatif terlebih dahulu');
}
$alternatifs = Makanan::whereIn('id', $idAlternatif)->get();
$kriterias = Kriteria::all();
// Ambil semua kriteria
$kriterias = \App\Models\Kriteria::all();
return view('admin.alternatif.perbandingan', compact('kriterias', 'alternatifs'));
// 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());
}
}
// 4. Simpan hasil perbandingan ke database
public function simpanPerbandingan(Request $request)
{
$data = $request->input('nilai');
foreach ($data as $kriteria_id => $baris) {
foreach ($baris as $alt1 => $kolom) {
foreach ($kolom as $alt2 => $nilai) {
if ($alt1 != $alt2 && $nilai != null) {
// Simpan nilai asli
PerbandinganAlternatif::updateOrCreate(
[
'kriteria_id' => $kriteria_id,
'alternatif_id_1' => $alt1,
'alternatif_id_2' => $alt2,
],
['nilai' => $nilai]
);
// Simpan nilai kebalikannya
PerbandinganAlternatif::updateOrCreate(
[
'kriteria_id' => $kriteria_id,
'alternatif_id_1' => $alt2,
'alternatif_id_2' => $alt1,
],
['nilai' => 1 / $nilai]
);
}
}
}
}
if ($request->input('action') == 'lanjut') {
return redirect()->route('alternatif.normalisasi')->with('success', 'Data berhasil disimpan, lanjut ke tahap normalisasi.');
}
return redirect()->back()->with('success', 'Perbandingan alternatif berhasil disimpan.');
}
public function tampilNormalisasi()
{
$idAlternatif = session('alternatifs_dipilih');
if (!$idAlternatif || count($idAlternatif) < 2) {
return redirect()->route('alternatif.pilih')->with('error', 'Pilih minimal dua alternatif terlebih dahulu.');
}
$alternatifs = Makanan::whereIn('id', $idAlternatif)->get();
$kriterias = Kriteria::all();
$matriks = [];
foreach ($kriterias as $kriteria) {
$matriks[$kriteria->id] = [];
foreach ($alternatifs as $alt1) {
$matriks[$kriteria->id][$alt1->id] = [];
foreach ($alternatifs as $alt2) {
if ($alt1->id == $alt2->id) {
$matriks[$kriteria->id][$alt1->id][$alt2->id] = 1;
} else {
$nilai = DB::table('perbandingan_alternatifs')
->where('kriteria_id', $kriteria->id)
->where('alternatif_id_1', $alt1->id)
->where('alternatif_id_2', $alt2->id)
->value('nilai');
$matriks[$kriteria->id][$alt1->id][$alt2->id] = $nilai ?? 1; // default 1
}
}
}
}
return view('admin.alternatif.normalisasi', compact('alternatifs', 'kriterias', 'matriks'));
}
public function simpanNormalisasi()
{
$idAlternatif = session('alternatifs_dipilih');
if (!$idAlternatif || count($idAlternatif) < 2) {
return redirect()->route('alternatif.pilih')->with('error', 'Pilih minimal dua alternatif terlebih dahulu.');
}
$alternatifs = Makanan::whereIn('id', $idAlternatif)->get();
$kriterias = Kriteria::all();
foreach ($kriterias as $kriteria) {
$jumlahKolom = [];
foreach ($alternatifs as $colAlt) {
$jumlahKolom[$colAlt->id] = 0;
}
// Hitung jumlah kolom
foreach ($alternatifs as $rowAlt) {
foreach ($alternatifs as $colAlt) {
$nilai = DB::table('perbandingan_alternatifs')
->where('kriteria_id', $kriteria->id)
->where('alternatif_id_1', $rowAlt->id)
->where('alternatif_id_2', $colAlt->id)
->value('nilai') ?? 1;
$jumlahKolom[$colAlt->id] += $nilai;
}
}
// Hitung bobot (normalisasi + rata-rata baris)
foreach ($alternatifs as $rowAlt) {
$jumlahBaris = 0;
foreach ($alternatifs as $colAlt) {
$nilai = DB::table('perbandingan_alternatifs')
->where('kriteria_id', $kriteria->id)
->where('alternatif_id_1', $rowAlt->id)
->where('alternatif_id_2', $colAlt->id)
->value('nilai') ?? 1;
$normalized = $nilai / $jumlahKolom[$colAlt->id];
$jumlahBaris += $normalized;
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');
}
$bobot = $jumlahBaris / count($alternatifs);
// 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 skor_makanan
SkorMakanan::updateOrCreate(
[
'kriteria_id' => $kriteria->id,
'makanan_id' => $rowAlt->id
],
[
'nilai' => $bobot
]
);
// 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());
}
}
return redirect()->route('rekomendasi.proses')->with('success', 'Bobot alternatif berhasil disimpan ke skor_makanan.');
}
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;
}
}

View File

@ -0,0 +1,97 @@
<?php
namespace App\Http\Controllers;
use App\Models\BobotKriteria;
use App\Models\PerbandinganKriteria;
use App\Models\PerbandinganAlternatif;
use App\Models\ConsistencyRatioAlternatif;
use App\Models\SkorMakanan;
use App\Models\Kriteria;
use App\Models\Makanan;
use Illuminate\Http\Request;
use Carbon\Carbon;
class HistoryAHPController extends Controller
{
public function index(Request $request)
{
try {
// Ambil tanggal dari request atau gunakan hari ini
$tanggal = $request->input('tanggal')
? Carbon::parse($request->input('tanggal'))->format('Y-m-d')
: now()->format('Y-m-d');
// Parse tanggal untuk query
$tanggalCarbon = Carbon::parse($tanggal)->startOfDay();
// Ambil data history perhitungan untuk tanggal yang dipilih
$bobotKriteria = [
$tanggal => BobotKriteria::with('kriteria')
->whereDate('created_at', $tanggalCarbon)
->orderBy('created_at', 'desc')
->get()
];
$perbandinganKriteria = [
$tanggal => PerbandinganKriteria::with(['kriteria1', 'kriteria2'])
->whereDate('created_at', $tanggalCarbon)
->orderBy('created_at', 'desc')
->get()
];
$perbandinganAlternatif = [
$tanggal => PerbandinganAlternatif::with(['alternatif1', 'alternatif2', 'kriteria'])
->whereDate('created_at', $tanggalCarbon)
->orderBy('created_at', 'desc')
->get()
];
$consistencyRatios = [
$tanggal => ConsistencyRatioAlternatif::with(['kriteria', 'komponen'])
->whereDate('created_at', $tanggalCarbon)
->orderBy('created_at', 'desc')
->get()
];
$skorMakanan = [
$tanggal => SkorMakanan::with(['makanan', 'kriteria'])
->whereDate('created_at', $tanggalCarbon)
->orderBy('created_at', 'desc')
->get()
];
// Filter out empty collections
$bobotKriteria = array_filter($bobotKriteria, function($collection) {
return $collection->isNotEmpty();
});
$perbandinganKriteria = array_filter($perbandinganKriteria, function($collection) {
return $collection->isNotEmpty();
});
$perbandinganAlternatif = array_filter($perbandinganAlternatif, function($collection) {
return $collection->isNotEmpty();
});
$consistencyRatios = array_filter($consistencyRatios, function($collection) {
return $collection->isNotEmpty();
});
$skorMakanan = array_filter($skorMakanan, function($collection) {
return $collection->isNotEmpty();
});
return view('admin.history.ahp', compact(
'bobotKriteria',
'perbandinganKriteria',
'perbandinganAlternatif',
'consistencyRatios',
'skorMakanan',
'tanggal'
));
} catch (\Exception $e) {
return redirect()->back()->with('error', 'Terjadi kesalahan saat memuat data history: ' . $e->getMessage());
}
}
}

View File

@ -9,9 +9,12 @@ class LandingPageController extends Controller
{
//
public function lp(){
$totalData = DB::table('makanans')->count(); // atau model: DataMakanan::count()
$totalData = DB::table('makanans')->count();
$totalKriteria = DB::table('kriterias')->count();
$totalUser = DB::table('users')->count();
$totalUser = DB::table('users')
->join('roles', 'users.role_id', '=', 'roles.id')
->where('roles.name', '=', 'user')
->count();
return view('landingpage', compact('totalData', 'totalKriteria', 'totalUser'));
}
}

View File

@ -27,7 +27,7 @@ class MakananController extends Controller
//
public function makanan(){
$makanans = Makanan::with('kategori', 'jenis')->get(); // Ambil semua data makanan dengan relasi kategori
$makanans = Makanan::with('kategori', 'jenis')->paginate(10); // Add pagination with 10 items per page
return view('admin.makanan.makanan', compact('makanans'));
}
@ -106,7 +106,7 @@ public function deletemakanan(Makanan $makanan) {
public function kategori(){
$kategoris = Kategori::all();
$kategoris = Kategori::paginate(10);
return view('admin.kategori.kategori', compact('kategoris'));
}
public function editkategori(Kategori $kategori){
@ -135,10 +135,10 @@ public function deletekategori(Kategori $kategori) {
public function jenismakanan()
{
$jenis_makanans = JenisMakanan::all();
return view('admin.jenismakanan.jenismakanan', compact('jenis_makanans'));
}
{
$jenis_makanans = JenisMakanan::paginate(10);
return view('admin.jenismakanan.jenismakanan', compact('jenis_makanans'));
}
public function editjenismakanan(JenisMakanan $jenis)
{
@ -173,7 +173,7 @@ public function deletejenismakanan(JenisMakanan $jenis)
public function komponen(){
$komponens = Komponen::all();
$komponens = Komponen::paginate(10); // Add pagination with 10 items per page
return view('admin.komponen.komponen', compact('komponens'));
}
public function editkomponen(Komponen $komponen){
@ -196,7 +196,7 @@ public function tambahkomponen(){
public function deletekomponen(Komponen $komponen) {
$komponen->delete();
return redirect()->route('komponen')->with('success', 'user deleted successfully');
return redirect()->route('komponen')->with('success', 'Komponen berhasil dihapus');
}
@ -207,7 +207,7 @@ public function deletekomponen(Komponen $komponen) {
public function waktumakan(){
$waktu_makans = WaktuMakan::all();
$waktu_makans = WaktuMakan::paginate(10); // Add pagination with 10 items per page
return view('admin.waktumakan.waktumakan', compact('waktu_makans'));
}
public function editwaktumakan(WaktuMakan $waktumakan){
@ -230,52 +230,42 @@ public function tambahwaktumakan(){
public function deletewaktumakan(WaktuMakan $waktumakan) {
$waktumakan->delete();
return redirect()->route('waktumakan')->with('success', 'user deleted successfully');
return redirect()->route('waktumakan')->with('success', 'Waktu makan berhasil dihapus');
}
public function index(Request $request)
{
$query = MakananKomponenWaktu::with(['makanan', 'komponen', 'waktuMakan']);
public function index()
{
$relasis = MakananKomponenWaktu::with(['makanan', 'komponen', 'waktuMakan'])
->orderBy('created_at', 'desc')
->paginate(10);
if ($request->komponen_id) {
$query->where('komponen_id', $request->komponen_id);
return view('admin.relasi.relasi', compact('relasis'));
}
if ($request->waktu_makan_id) {
$query->where('waktu_makan_id', $request->waktu_makan_id);
}
$relasis = $query->get();
$makanans = Makanan::all();
$komponens = Komponen::all();
$waktuMakans = WaktuMakan::all();
return view('admin.relasi.relasi', compact('relasis', 'makanans', 'komponens', 'waktuMakans'));
}
public function store(Request $request)
{
$request->validate([
$validatedData = $request->validate([
'makanan_id' => 'required|exists:makanans,id',
'komponen_id' => 'required|exists:komponens,id',
'waktu_makan_id' => 'required|exists:waktu_makans,id',
'status' => 'required|boolean'
]);
// Cegah duplikasi
$exists = MakananKomponenWaktu::where([
'makanan_id' => $request->makanan_id,
'komponen_id' => $request->komponen_id,
'waktu_makan_id' => $request->waktu_makan_id
])->exists();
if (!$exists) {
MakananKomponenWaktu::create($request->only(['makanan_id', 'komponen_id', 'waktu_makan_id']));
return redirect()->back()->with('success', 'Relasi berhasil ditambahkan.');
} else {
return redirect()->back()->with('error', 'Relasi sudah ada.');
try {
MakananKomponenWaktu::create($validatedData);
return redirect()->route('relasi.index')->with('success', 'Relasi berhasil ditambahkan!');
} catch (\Illuminate\Database\QueryException $e) {
if ($e->errorInfo[1] == 1062) { // MySQL error code for duplicate entry
return redirect()->back()
->withInput()
->with('error', 'Relasi ini sudah ada dalam database.');
}
return redirect()->back()
->withInput()
->with('error', 'Terjadi kesalahan: ' . $e->getMessage());
}
}
@ -291,32 +281,31 @@ public function edit($id)
public function update(Request $request, $id)
{
$request->validate([
$validatedData = $request->validate([
'makanan_id' => 'required|exists:makanans,id',
'komponen_id' => 'required|exists:komponens,id',
'waktu_makan_id' => 'required|exists:waktu_makans,id',
'status' => 'required|boolean'
]);
$relasi = MakananKomponenWaktu::findOrFail($id);
// Cek apakah ada duplikasi selain record yang sedang diupdate
$exists = MakananKomponenWaktu::where('id', '!=', $id)
->where('makanan_id', $request->makanan_id)
->where('komponen_id', $request->komponen_id)
->where('waktu_makan_id', $request->waktu_makan_id)
->exists();
if ($exists) {
return redirect()->back()->with('error', 'Relasi sudah ada.');
try {
$relasi = MakananKomponenWaktu::findOrFail($id);
$relasi->update($validatedData);
return redirect()->route('relasi.index')->with('success', 'Relasi berhasil diperbarui!');
} catch (\Illuminate\Database\QueryException $e) {
if ($e->errorInfo[1] == 1062) { // MySQL error code for duplicate entry
return redirect()->back()
->withInput()
->with('error', 'Relasi ini sudah ada dalam database.');
}
return redirect()->back()
->withInput()
->with('error', 'Terjadi kesalahan: ' . $e->getMessage());
} catch (\Exception $e) {
return redirect()->back()
->withInput()
->with('error', 'Terjadi kesalahan: ' . $e->getMessage());
}
$relasi->update([
'makanan_id' => $request->makanan_id,
'komponen_id' => $request->komponen_id,
'waktu_makan_id' => $request->waktu_makan_id,
]);
return redirect()->route('relasi')->with('success', 'Relasi berhasil diperbarui.');
}
@ -327,4 +316,13 @@ public function destroy($id)
return redirect()->back()->with('success', 'Relasi berhasil dihapus.');
}
public function create()
{
$makanans = Makanan::all();
$komponens = Komponen::all();
$waktuMakans = WaktuMakan::all();
return view('admin.relasi.create', compact('makanans', 'komponens', 'waktuMakans'));
}
}

View File

@ -0,0 +1,213 @@
<?php
namespace App\Http\Controllers;
use App\Models\UserNotification;
use App\Services\NotificationService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class NotificationController extends Controller
{
public function index()
{
$user = Auth::user();
$notifications = $user->notifications()
->orderBy('created_at', 'desc')
->paginate(20);
$unreadCount = $user->unreadNotifications()->count();
return view('user.notifications.index', compact('notifications', 'unreadCount'));
}
public function markAsRead($id)
{
$success = NotificationService::markAsRead($id, Auth::id());
return response()->json([
'success' => $success,
'message' => $success ? 'Notifikasi ditandai sebagai dibaca' : 'Notifikasi tidak ditemukan'
]);
}
public function markAllAsRead()
{
$count = NotificationService::markAllAsRead(Auth::id());
return response()->json([
'success' => true,
'message' => "{$count} notifikasi ditandai sebagai dibaca"
]);
}
public function delete($id)
{
$notification = UserNotification::where('id', $id)
->where('user_id', Auth::id())
->first();
if ($notification) {
$notification->delete();
return response()->json([
'success' => true,
'message' => 'Notifikasi berhasil dihapus'
]);
}
return response()->json([
'success' => false,
'message' => 'Notifikasi tidak ditemukan'
]);
}
public function getUnreadCount()
{
$count = Auth::user()->unreadNotifications()->count();
return response()->json([
'count' => $count
]);
}
public function getLatestNotifications()
{
$notifications = Auth::user()->notifications()
->orderBy('created_at', 'desc')
->limit(5)
->get();
return response()->json([
'notifications' => $notifications
]);
}
public function testNotification()
{
$user = Auth::user();
$notification = NotificationService::sendGeneralNotification(
$user->id,
'Test Notifikasi',
'Ini adalah test notifikasi dari sistem. Sistem notifikasi berfungsi dengan baik!',
'info',
['test' => true]
);
return response()->json([
'success' => true,
'message' => 'Test notifikasi berhasil dikirim',
'notification' => $notification
]);
}
public function testBrowserNotification()
{
$user = Auth::user();
// Kirim notifikasi ke database
$notification = NotificationService::sendGeneralNotification(
$user->id,
'Test Browser Notification',
'Ini adalah test notifikasi browser yang akan muncul sebagai popup di device Anda.',
'info',
['browser_test' => true, 'timestamp' => now()]
);
// Data untuk browser notification
$notificationData = [
'title' => 'HeartChoice - Test Browser Notification',
'body' => 'Ini adalah test notifikasi popup di device Anda. Jika Anda melihat ini, berarti notifikasi browser berfungsi dengan baik!',
'icon' => asset('logo/baru/dutdut.png'),
'badge' => asset('logo/baru/dutdut.png'),
'tag' => 'test-browser-notification',
'requireInteraction' => false,
'silent' => false,
'vibrate' => [200, 100, 200],
'data' => [
'url' => route('user.notifications'),
'notification_id' => $notification->id,
'type' => 'browser_test'
]
];
return response()->json([
'success' => true,
'message' => 'Test browser notification berhasil dikirim',
'notification' => $notification,
'browser_data' => $notificationData
]);
}
public function sendBackgroundNotification(Request $request)
{
$user = Auth::user();
$title = $request->input('title', 'HeartChoice Notification');
$body = $request->input('body', 'Anda memiliki notifikasi baru!');
$type = $request->input('type', 'info');
// Kirim notifikasi ke database
$notification = NotificationService::sendGeneralNotification(
$user->id,
$title,
$body,
$type,
['background' => true, 'timestamp' => now()]
);
// Data untuk background notification
$notificationData = [
'title' => $title,
'body' => $body,
'icon' => asset('logo/baru/dutdut.png'),
'badge' => asset('logo/baru/dutdut.png'),
'tag' => 'background-notification',
'requireInteraction' => false,
'silent' => false,
'vibrate' => [200, 100, 200],
'data' => [
'url' => route('user.notifications'),
'notification_id' => $notification->id,
'type' => 'background'
]
];
return response()->json([
'success' => true,
'message' => 'Background notification berhasil dikirim',
'notification' => $notification,
'browser_data' => $notificationData
]);
}
public function getAvailableSounds()
{
$audioPath = public_path('audio');
$availableSounds = [];
if (is_dir($audioPath)) {
$files = scandir($audioPath);
foreach ($files as $file) {
if ($file !== '.' && $file !== '..') {
$extension = strtolower(pathinfo($file, PATHINFO_EXTENSION));
// Hanya ambil file audio
if (in_array($extension, ['mp3', 'wav', 'ogg', 'm4a'])) {
$name = pathinfo($file, PATHINFO_FILENAME);
$availableSounds[] = [
'name' => $name,
'file' => $file,
'path' => asset("audio/{$file}"),
'display_name' => ucfirst(str_replace(['_', '-'], ' ', $name))
];
}
}
}
}
return response()->json([
'sounds' => $availableSounds
]);
}
}

View File

@ -12,7 +12,7 @@ class PerbandinganKriteriaController extends Controller
{
//
public function kriteria(){
$kriterias = Kriteria::all();
$kriterias = Kriteria::paginate(10);
return view('admin.kriteria.kriteria', compact('kriterias'));
}
public function editkriteria(Kriteria $kriteria){

View File

@ -7,11 +7,31 @@
use App\Models\Alternatif;
use App\Models\PerbandinganKriteria;
use App\Models\BobotKriteria;
use App\Models\ConsistencyRatioCriteria;
use App\Models\Makanan;
use Illuminate\Support\Facades\Session;
class ProsesController extends Controller
{
// Random Index value for n criteria (1-15)
private $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
];
public function showPerbandingan()
{
$kriterias = Kriteria::all();
@ -22,17 +42,24 @@ public function prosesSementara(Request $request)
{
$request->validate([
'nilai' => 'required|array',
'waktu_makan_id' => 'required|exists:waktu_makans,id'
]);
// Simpan waktu_makan_id ke session
Session::put('waktu_makan_id', $request->waktu_makan_id);
$waktuMakanId = $request->waktu_makan_id;
$kriterias = Kriteria::all();
$nilai = [];
// Hapus data perbandingan lama untuk waktu makan ini
foreach ($kriterias as $baris) {
foreach ($kriterias as $kolom) {
if ($baris->id != $kolom->id) {
PerbandinganKriteria::where([
['kriteria_id_1', $baris->id],
['kriteria_id_2', $kolom->id]
['kriteria_id_2', $kolom->id],
['waktu_makan_id', $waktuMakanId]
])->delete();
}
}
@ -47,10 +74,12 @@ public function prosesSementara(Request $request)
$nilai[$baris->id][$kolom->id] = $val;
$nilai[$kolom->id][$baris->id] = 1 / $val;
// Simpan ke database dengan waktu_makan_id
PerbandinganKriteria::updateOrCreate(
[
'kriteria_id_1' => $baris->id,
'kriteria_id_2' => $kolom->id
'kriteria_id_2' => $kolom->id,
'waktu_makan_id' => $waktuMakanId
],
[
'nilai' => $val
@ -61,66 +90,126 @@ public function prosesSementara(Request $request)
}
Session::put('matriks', $nilai);
return redirect()->route('perbandingan');
return redirect()->route('perbandingan')
->with('success', 'Perbandingan kriteria berhasil disimpan.');
}
public function simpanPerbandingan()
public function simpanPerbandingan(Request $request)
{
$matriks = Session::get('matriks');
$waktuMakanId = Session::get('waktu_makan_id');
if (!$matriks) {
return redirect()->route('perbandingan')->with('error', 'Belum ada data untuk disimpan.');
return redirect()->route('perbandingan')
->with('error', 'Belum ada data untuk disimpan.');
}
if (!$waktuMakanId) {
return redirect()->route('perbandingan')
->with('error', 'Waktu makan belum dipilih.');
}
// Simpan matriks dan waktu makan ke session
Session::put('matriks_perbandingan', $matriks);
return redirect()->route('hasil.normalisasi')->with('success', 'Data berhasil disimpan!');
Session::put('waktu_makan_id', $waktuMakanId);
return redirect()->route('hasil.normalisasi')
->with('success', 'Data perbandingan berhasil disimpan!');
}
public function hasilNormalisasi()
public function normalisasi()
{
$kriterias = Kriteria::all();
$matriks = Session::get('matriks_perbandingan');
$waktuMakanId = Session::get('waktu_makan_id');
if (!$matriks) {
return redirect()->route('perbandingan')->with('error', 'Data perbandingan belum tersedia.');
}
$jumlahKolom = [];
foreach ($kriterias as $kriteria) {
$id = $kriteria->id;
$jumlahKolom[$id] = 0;
foreach ($matriks as $baris) {
$jumlahKolom[$id] += $baris[$id];
}
if (!$matriks || !$waktuMakanId) {
return redirect()->route('perbandingan')
->with('error', 'Belum ada data untuk dinormalisasi.');
}
$kriterias = Kriteria::all();
$normalisasi = [];
foreach ($kriterias as $baris) {
$row = [];
foreach ($kriterias as $kolom) {
$row[] = $matriks[$baris->id][$kolom->id] / $jumlahKolom[$kolom->id];
}
$normalisasi[] = $row;
}
$jumlahKolom = [];
$bobot = [];
foreach ($normalisasi as $baris) {
$bobot[] = array_sum($baris) / count($baris);
$totalBobot = 0;
// Hitung jumlah kolom
foreach ($kriterias as $kolom) {
$jumlahKolom[$kolom->id] = 0;
foreach ($kriterias as $baris) {
$jumlahKolom[$kolom->id] += $matriks[$baris->id][$kolom->id];
}
}
foreach ($bobot as $index => $value) {
// Normalisasi
foreach ($kriterias as $baris) {
foreach ($kriterias as $kolom) {
$normalisasi[$baris->id][$kolom->id] = $matriks[$baris->id][$kolom->id] / $jumlahKolom[$kolom->id];
}
}
// Hitung bobot
foreach ($kriterias as $kriteria) {
$bobot[$kriteria->id] = array_sum($normalisasi[$kriteria->id]) / count($kriterias);
$totalBobot += $bobot[$kriteria->id];
}
// Normalisasi bobot
foreach ($bobot as $key => $value) {
$bobot[$key] = $value / $totalBobot;
}
// Hitung CI dan CR
$lambdaMax = 0;
foreach ($kriterias as $kriteria) {
$sum = 0;
foreach ($kriterias as $k) {
$sum += $matriks[$kriteria->id][$k->id] * $bobot[$k->id];
}
$lambdaMax += $sum / $bobot[$kriteria->id];
}
$lambdaMax = $lambdaMax / count($kriterias);
$CI = ($lambdaMax - count($kriterias)) / (count($kriterias) - 1);
$CR = $CI / $this->RI[count($kriterias)];
// Simpan bobot ke database
foreach ($bobot as $kriteriaId => $value) {
BobotKriteria::updateOrCreate(
['kriteria_id' => $kriterias[$index]->id],
[
'kriteria_id' => $kriteriaId,
'waktu_makan_id' => $waktuMakanId
],
['bobot' => $value]
);
}
// Simpan CR ke database
ConsistencyRatioCriteria::updateOrCreate(
[
'waktu_makan_id' => $waktuMakanId
],
[
'ci' => $CI,
'cr' => $CR,
'is_consistent' => $CR <= 0.1
]
);
Session::put('bobot_kriteria', $bobot);
Session::put('tahap_kriteria_selesai', true);
// Ambil data waktu makan
$waktuMakan = \App\Models\WaktuMakan::findOrFail($waktuMakanId);
return view('admin.proses.normalisasi', [
'kriterias' => $kriterias,
'normalisasi' => $normalisasi,
'bobot' => $bobot
'bobot' => $bobot,
'ci' => $CI,
'cr' => $CR,
'is_consistent' => $CR <= 0.1,
'waktu_makan_id' => $waktuMakanId,
'waktu_makan' => $waktuMakan // Tambahkan ini untuk menghindari query di view
]);
}
}

View File

@ -0,0 +1,72 @@
<?php
namespace App\Http\Controllers;
use App\Models\RekomendasiAhli;
use App\Models\WaktuMakan;
use App\Models\Komponen;
use App\Models\Makanan;
use Illuminate\Http\Request;
class RekomendasiAhliController extends Controller
{
public function index()
{
// Ambil semua data rekomendasi ahli beserta relasi
$rekomendasiAhli = \App\Models\RekomendasiAhli::with(['komponen', 'makanan', 'waktuMakan'])->get();
// Group by hari
$rekomendasiByHari = $rekomendasiAhli->groupBy('hari');
// Ambil semua hari (urutan enum)
$daftarHari = ['Senin','Selasa','Rabu','Kamis','Jumat','Sabtu','Minggu'];
return view('admin.rekomendasi-ahli-list', compact('rekomendasiByHari', 'daftarHari'));
}
public function create(Request $request)
{
$hari = $request->query('hari');
$waktuMakans = WaktuMakan::all();
$komponens = Komponen::all();
$makanans = Makanan::all();
return view('admin.rekomendasi-ahli.create', compact('hari', 'waktuMakans', 'komponens', 'makanans'));
}
public function store(Request $request)
{
$request->validate([
'makanan_id' => 'required|exists:makanans,id',
'komponen_id' => 'required|exists:komponens,id',
'waktu_makan_id' => 'required|exists:waktu_makans,id',
'hari' => 'required'
]);
RekomendasiAhli::create($request->all());
return redirect()->route('rekomendasi_ahli.index')->with('success', 'Rekomendasi ahli berhasil ditambahkan.');
}
public function edit($id)
{
$rekomendasi = RekomendasiAhli::findOrFail($id);
$waktuMakans = WaktuMakan::all();
$komponens = Komponen::all();
$makanans = Makanan::all();
return view('admin.rekomendasi-ahli.edit', compact('rekomendasi', 'waktuMakans', 'komponens', 'makanans'));
}
public function update(Request $request, $id)
{
$request->validate([
'makanan_id' => 'required|exists:makanans,id',
'komponen_id' => 'required|exists:komponens,id',
'waktu_makan_id' => 'required|exists:waktu_makans,id',
'hari' => 'required'
]);
$rekomendasi = RekomendasiAhli::findOrFail($id);
$rekomendasi->update($request->all());
return redirect()->route('rekomendasi_ahli.index')->with('success', 'Rekomendasi ahli berhasil diupdate.');
}
public function destroy($id)
{
RekomendasiAhli::findOrFail($id)->delete();
return redirect()->route('rekomendasi_ahli.index')->with('success', 'Rekomendasi ahli berhasil dihapus.');
}
}

View File

@ -3,136 +3,552 @@
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Makanan;
use App\Models\Kriteria;
use App\Models\Makanan;
use App\Models\BobotKriteria;
use App\Models\SkorMakanan;
use App\Models\Rekomendasi;
use App\Models\WaktuMakan;
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()
{
$kriterias = Kriteria::all();
$defaultBobots = BobotKriteria::pluck('bobot', 'kriteria_id');
$waktuMakans = WaktuMakan::all();
$komponens = Komponen::all();
{
try {
// Ambil data dari session
$sessionData = session('hasil_rekomendasi');
$alternatifsByKomponen = session('alternatifs_by_komponen');
$waktuMakanId = session('waktu_makan_id');
SkorMakanan::truncate();
Rekomendasi::where('user_id', Auth::id())->delete();
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.');
}
foreach ($waktuMakans as $waktu) {
// Ambil preferensi bobot kriteria untuk waktu makan ini
$preferensiBobot = \App\Models\PreferensiWaktuKriteria::where('waktu_makan_id', $waktu->id)
->pluck('bobot', 'kriteria_id');
foreach ($komponens as $komponen) {
$makanans = Makanan::whereHas('komponenWaktu', function ($query) use ($komponen, $waktu) {
$query->where('komponen_id', $komponen->id)
->where('waktu_makan_id', $waktu->id);
})->get();
if ($makanans->isEmpty()) continue;
$total = [
'lemak' => $makanans->sum('lemak') ?: 1,
'natrium' => $makanans->sum('natrium') ?: 1,
'energi' => $makanans->sum('energi') ?: 1,
'karbohidrat' => $makanans->sum('karbohidrat') ?: 1,
];
foreach ($makanans as $makanan) {
$skor = 0;
// 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) {
$attr = strtolower($kriteria->nama);
$nilai = $makanan->$attr ?? 0;
// Normalisasi
$normalized = in_array($attr, ['lemak', 'natrium'])
? 1 - ($nilai / $total[$attr])
: $nilai / $total[$attr];
// Gunakan preferensi waktu jika ada, jika tidak fallback
$bobot = $preferensiBobot[$kriteria->id] ?? $defaultBobots[$kriteria->id] ?? 0;
$skor += $normalized * $bobot;
SkorMakanan::updateOrCreate([
'kriteria_id' => $kriteria->id,
'makanan_id' => $makanan->id,
], [
'nilai' => $normalized
]);
$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;
}
}
}
}
// Simpan nilai akhir ke rekomendasi
Rekomendasi::updateOrCreate(
[
'user_id' => Auth::id(),
'makanan_id' => $makanan->id,
'komponen_id' => $komponen->id,
'waktu_makan_id' => $waktu->id,
],
[
'nilai_akhir' => $skor,
'tanggal_rekomendasi' => now(),
]
);
// 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());
}
}
return redirect()->route('rekomendasi.hasil')->with('success', 'Rekomendasi berhasil dihitung dengan bobot preferensi waktu.');
}
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()
{
$rekomendasi = Rekomendasi::with('makanan', 'komponen', 'waktuMakan')
->where('user_id', Auth::id())
// ->whereDate('tanggal_rekomendasi', now())
->get();
// Urutan komponen manual
$urutanKomponen = ['karbohidrat', 'protein', 'sayur', 'buah', 'susu'];
// Hitung total nilai_akhir per waktu & komponen
$totalPerGroup = [];
foreach ($rekomendasi as $r) {
$waktu = strtolower($r->waktuMakan->nama);
$komponen = strtolower($r->komponen->nama);
$totalPerGroup[$waktu][$komponen] = ($totalPerGroup[$waktu][$komponen] ?? 0) + $r->nilai_akhir;
}
// Group dan sort data
$rekomendasiGrouped = $rekomendasi
->groupBy([
fn($item) => strtolower($item->waktuMakan->nama),
fn($item) => strtolower($item->komponen->nama),
]);
foreach ($rekomendasiGrouped as $waktu => $komponens) {
$sorted = collect();
foreach ($urutanKomponen as $target) {
if ($komponens->has($target)) {
// Tambahkan properti persentase ke setiap item
$komponens[$target]->each(function ($item) use ($totalPerGroup, $waktu, $target) {
$total = $totalPerGroup[$waktu][$target] ?: 1;
$item->persentase = ($item->nilai_akhir / $total) * 100;
});
$sorted[$target] = $komponens[$target]->sortByDesc('nilai_akhir');
{
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());
}
$rekomendasiGrouped[$waktu] = $sorted;
}
return view('admin.rekomendasi', ['rekomendasi' => $rekomendasiGrouped]);
}
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());
}
}
}

View File

@ -0,0 +1,355 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Makanan;
use App\Models\Komponen;
use App\Models\WaktuMakan;
use App\Models\MakananKomponenWaktu;
use App\Models\PerbandinganAlternatif;
use App\Models\SkorMakanan;
use App\Models\Kriteria;
use App\Models\BobotKriteria;
use App\Models\ConsistencyRatioAlternatif;
use App\Models\SimulasiAhp;
use App\Models\SimulasiAhpPairwise;
use App\Models\SimulasiAhpNormalisasi;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\Auth;
class SimulasiController extends Controller
{
public function index(Request $request)
{
$waktuMakans = WaktuMakan::all();
$komponens = Komponen::all();
$kriterias = Kriteria::all();
$makanans = null;
$waktuMakanId = $request->waktu_makan_id;
$komponenId = $request->komponen_id;
if ($waktuMakanId && $komponenId) {
$query = MakananKomponenWaktu::with('makanan')
->where('waktu_makan_id', $waktuMakanId);
$makananKomponenWaktu = $query->where('status', true)->get();
if ($makananKomponenWaktu->isEmpty()) {
$makananKomponenWaktu = MakananKomponenWaktu::with('makanan')
->where('waktu_makan_id', $waktuMakanId)
->get();
}
$filtered = $makananKomponenWaktu->where('komponen_id', $komponenId);
$makanans = $filtered->map(function($item) {
$makanan = $item->makanan;
$makanan->komponen_id = $item->komponen_id;
return $makanan;
})->values();
}
return view('user.simulasi.index', compact('waktuMakans', 'komponens', 'kriterias', 'makanans'));
}
public function getMakananByKomponen($komponen, $waktuMakan)
{
// Ambil semua data makanan_komponen_waktu untuk waktu makan tertentu
$query = MakananKomponenWaktu::with('makanan')
->where('waktu_makan_id', $waktuMakan);
// Coba filter status true dulu
$makananKomponenWaktu = $query->where('status', true)->get();
// Jika kosong, ambil semua tanpa filter status
if ($makananKomponenWaktu->isEmpty()) {
$makananKomponenWaktu = MakananKomponenWaktu::with('makanan')
->where('waktu_makan_id', $waktuMakan)
->get();
}
// Filter berdasarkan komponen
$filtered = $makananKomponenWaktu->where('komponen_id', $komponen);
// Map ke array makanan, tambahkan komponen_id
$makanans = $filtered->map(function($item) {
$makanan = $item->makanan;
$makanan->komponen_id = $item->komponen_id;
return $makanan;
})->values();
return response()->json($makanans);
}
public function prosesSimulasi(Request $request)
{
try {
DB::beginTransaction();
// Validasi minimal 4 makanan
$selectedMakanans = collect($request->makanan_ids)->filter()->values();
if ($selectedMakanans->count() < 4) {
return redirect()->back()->with('error', 'Pilih minimal 4 makanan untuk simulasi');
}
$waktuMakanId = $request->waktu_makan_id;
$komponenId = $request->komponen_id;
$kriterias = Kriteria::all();
// Ambil bobot kriteria berdasarkan waktu makan
$bobotKriteria = [];
foreach ($kriterias as $kriteria) {
$bobot = BobotKriteria::where('kriteria_id', $kriteria->id)
->where('waktu_makan_id', $waktuMakanId)
->first();
$bobotKriteria[$kriteria->id] = $bobot ? $bobot->bobot : 0;
}
// Normalisasi bobot kriteria
$totalBobot = array_sum($bobotKriteria);
if ($totalBobot > 0) {
foreach ($bobotKriteria as $key => $value) {
$bobotKriteria[$key] = $value / $totalBobot;
}
}
// Buat record simulasi baru
$simulasi = SimulasiAhp::create([
'user_id' => Auth::id(),
'judul' => 'Simulasi ' . WaktuMakan::find($waktuMakanId)->nama . ' - ' . Komponen::find($komponenId)->nama,
'pilihan_makanan_ids' => $selectedMakanans->toArray(),
'hasil_rangking' => []
]);
// Proses perbandingan berpasangan untuk setiap kriteria
foreach ($kriterias as $kriteria) {
$matriksPerbandingan = [];
$jumlahKolom = [];
// Inisialisasi matriks
foreach ($selectedMakanans as $makananId) {
$jumlahKolom[$makananId] = 0;
foreach ($selectedMakanans as $makananId2) {
$matriksPerbandingan[$makananId][$makananId2] = 1;
}
}
// Hitung nilai perbandingan berdasarkan kriteria
foreach ($selectedMakanans as $makananId1) {
$makanan1 = Makanan::find($makananId1);
foreach ($selectedMakanans as $makananId2) {
if ($makananId1 != $makananId2) {
$makanan2 = Makanan::find($makananId2);
$nilai1 = $this->getNilaiKriteria($makanan1, $kriteria);
$nilai2 = $this->getNilaiKriteria($makanan2, $kriteria);
if ($nilai1 > 0 && $nilai2 > 0) {
// Terapkan nilai invers untuk kriteria cost (lemak dan natrium)
if (in_array(strtolower($kriteria->nama), ['lemak', 'natrium'])) {
$ratio = $nilai2 / $nilai1; // Invers untuk cost
} else {
$ratio = $nilai1 / $nilai2;
}
if ($ratio > 9) $ratio = 9;
if ($ratio < 1/9) $ratio = 1/9;
$matriksPerbandingan[$makananId1][$makananId2] = $ratio;
$matriksPerbandingan[$makananId2][$makananId1] = 1 / $ratio;
}
}
}
}
// Simpan matriks perbandingan berpasangan
foreach ($selectedMakanans as $makananId1) {
foreach ($selectedMakanans as $makananId2) {
if ($makananId1 != $makananId2) {
SimulasiAhpPairwise::create([
'simulasi_ahp_id' => $simulasi->id,
'makanan_1_id' => $makananId1,
'makanan_2_id' => $makananId2,
'nilai' => $matriksPerbandingan[$makananId1][$makananId2]
]);
}
}
}
// Hitung jumlah kolom
foreach ($selectedMakanans as $makananId2) {
foreach ($selectedMakanans as $makananId1) {
$jumlahKolom[$makananId2] += $matriksPerbandingan[$makananId1][$makananId2];
}
}
// Normalisasi matriks dan hitung priority vector
$matriksNormal = [];
$priorityVector = [];
foreach ($selectedMakanans as $makananId1) {
$jumlahBaris = 0;
foreach ($selectedMakanans as $makananId2) {
if ($jumlahKolom[$makananId2] != 0) {
$nilai = $matriksPerbandingan[$makananId1][$makananId2] / $jumlahKolom[$makananId2];
$matriksNormal[$makananId1][$makananId2] = $nilai;
$jumlahBaris += $nilai;
}
}
$priorityVector[$makananId1] = $jumlahBaris / count($selectedMakanans);
}
// Simpan hasil normalisasi
foreach ($selectedMakanans as $makananId) {
SimulasiAhpNormalisasi::create([
'simulasi_ahp_id' => $simulasi->id,
'makanan_id' => $makananId,
'nilai_normalisasi' => $priorityVector[$makananId],
'bobot_akhir' => $priorityVector[$makananId] * $bobotKriteria[$kriteria->id]
]);
}
// Hitung Consistency Ratio
$n = count($selectedMakanans);
if ($n > 1) {
$lambdaMax = 0;
foreach ($selectedMakanans as $makananId1) {
$sum = 0;
foreach ($selectedMakanans as $makananId2) {
$sum += $matriksPerbandingan[$makananId1][$makananId2] * $priorityVector[$makananId2];
}
$lambdaMax += $sum / $priorityVector[$makananId1];
}
$lambdaMax /= $n;
$CI = ($lambdaMax - $n) / ($n - 1);
$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
];
$CR = $CI / ($RI[$n] ?? end($RI));
// Simpan CR ke database
ConsistencyRatioAlternatif::updateOrCreate(
[
'kriteria_id' => $kriteria->id,
'waktu_makan_id' => $waktuMakanId,
'komponen_id' => $komponenId
],
[
'ci' => $CI,
'cr' => $CR,
'is_consistent' => $CR <= 0.1
]
);
}
// Simpan skor ke database
foreach ($selectedMakanans as $makananId) {
SkorMakanan::updateOrCreate(
[
'makanan_id' => $makananId,
'kriteria_id' => $kriteria->id,
'waktu_makan_id' => $waktuMakanId,
'komponen_id' => $komponenId
],
['nilai' => $priorityVector[$makananId]]
);
}
}
// Hitung skor akhir
$hasilRekomendasi = [];
foreach ($selectedMakanans as $makananId) {
$skorAkhir = 0;
foreach ($kriterias as $kriteria) {
$skor = SkorMakanan::where([
'makanan_id' => $makananId,
'kriteria_id' => $kriteria->id,
'waktu_makan_id' => $waktuMakanId,
'komponen_id' => $komponenId
])->first();
if ($skor) {
$skorAkhir += $skor->nilai * $bobotKriteria[$kriteria->id];
}
}
$hasilRekomendasi[$makananId] = $skorAkhir;
}
// Urutkan hasil berdasarkan skor
arsort($hasilRekomendasi);
// Update hasil ranking di tabel simulasi
$simulasi->update(['hasil_rangking' => $hasilRekomendasi]);
// Simpan hasil ke session
Session::put('hasil_simulasi', [
'simulasi_id' => $simulasi->id,
'waktu_makan_id' => $waktuMakanId,
'komponen_id' => $komponenId,
'hasil' => $hasilRekomendasi,
'makanan_ids' => $selectedMakanans->toArray(),
'bobot_kriteria' => $bobotKriteria
]);
DB::commit();
return redirect()->route('user.simulasi.hasil');
} catch (\Exception $e) {
DB::rollback();
return redirect()->back()->with('error', 'Terjadi kesalahan: ' . $e->getMessage());
}
}
public function hasil()
{
$hasilSimulasi = Session::get('hasil_simulasi');
if (!$hasilSimulasi) {
return redirect()->route('user.simulasi.index');
}
$waktuMakan = WaktuMakan::find($hasilSimulasi['waktu_makan_id']);
$komponen = Komponen::find($hasilSimulasi['komponen_id']);
$makanans = Makanan::whereIn('id', $hasilSimulasi['makanan_ids'])->get();
$kriterias = Kriteria::all();
$crCriteria = \App\Models\ConsistencyRatioCriteria::where('waktu_makan_id', $waktuMakan->id ?? $hasilSimulasi['waktu_makan_id'])->orderByDesc('created_at')->first();
return view('user.simulasi.hasil', compact('hasilSimulasi', 'waktuMakan', 'komponen', 'makanans', 'kriterias', 'crCriteria'));
}
public function history()
{
$simulasis = SimulasiAhp::where('user_id', Auth::id())
->orderBy('created_at', 'desc')
->paginate(10);
return view('user.simulasi.history', compact('simulasis'));
}
public function detailHistory($id)
{
$simulasi = SimulasiAhp::where('user_id', Auth::id())
->where('id', $id)
->firstOrFail();
$makanans = Makanan::whereIn('id', $simulasi->pilihan_makanan_ids)->get();
$pairwise = SimulasiAhpPairwise::where('simulasi_ahp_id', $id)->get();
$normalisasi = SimulasiAhpNormalisasi::where('simulasi_ahp_id', $id)->get();
return view('user.simulasi.detail-history', compact('simulasi', 'makanans', 'pairwise', 'normalisasi'));
}
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;
}
}
}

View File

@ -0,0 +1,103 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Carbon\Carbon;
use DateTimeZone;
class TimezoneController extends Controller
{
public function index()
{
$currentTimezone = config('app.timezone');
$currentTime = Carbon::now();
$availableTimezones = $this->getAvailableTimezones();
return view('user.timezone', compact('currentTimezone', 'currentTime', 'availableTimezones'));
}
public function update(Request $request)
{
$timezone = $request->input('timezone');
// Validate timezone
if (!in_array($timezone, DateTimeZone::listIdentifiers())) {
return redirect()->back()->with('error', 'Timezone tidak valid!');
}
// Update .env file
$this->updateEnvFile('APP_TIMEZONE', $timezone);
// Clear config cache
\Artisan::call('config:clear');
return redirect()->back()->with('success', 'Timezone berhasil diperbarui ke ' . $timezone);
}
public function getCurrentTime()
{
$timezone = request('timezone', config('app.timezone'));
$currentTime = Carbon::now($timezone);
return response()->json([
'timezone' => $timezone,
'current_time' => $currentTime->format('Y-m-d H:i:s'),
'formatted_time' => $currentTime->format('d M Y H:i:s'),
'day_name' => $currentTime->format('l'),
'time_only' => $currentTime->format('H:i:s'),
'date_only' => $currentTime->format('d M Y')
]);
}
private function getAvailableTimezones()
{
$timezones = [
'Asia/Jakarta' => 'WIB (UTC+7)',
'Asia/Makassar' => 'WITA (UTC+8)',
'Asia/Jayapura' => 'WIT (UTC+9)',
'UTC' => 'UTC (UTC+0)',
'Asia/Singapore' => 'Singapore (UTC+8)',
'Asia/Kuala_Lumpur' => 'Malaysia (UTC+8)',
'Asia/Bangkok' => 'Thailand (UTC+7)',
'Asia/Manila' => 'Philippines (UTC+8)',
'Asia/Ho_Chi_Minh' => 'Vietnam (UTC+7)',
'Asia/Seoul' => 'Korea (UTC+9)',
'Asia/Tokyo' => 'Japan (UTC+9)',
'Asia/Shanghai' => 'China (UTC+8)',
'America/New_York' => 'New York (UTC-5)',
'America/Los_Angeles' => 'Los Angeles (UTC-8)',
'Europe/London' => 'London (UTC+0)',
'Europe/Paris' => 'Paris (UTC+1)',
'Europe/Berlin' => 'Berlin (UTC+1)',
'Australia/Sydney' => 'Sydney (UTC+10)',
'Australia/Perth' => 'Perth (UTC+8)',
];
return $timezones;
}
private function updateEnvFile($key, $value)
{
$path = base_path('.env');
if (file_exists($path)) {
$content = file_get_contents($path);
// Check if key exists
if (strpos($content, $key . '=') !== false) {
// Update existing key
$content = preg_replace(
'/^' . $key . '=.*/m',
$key . '=' . $value,
$content
);
} else {
// Add new key
$content .= "\n" . $key . '=' . $value;
}
file_put_contents($path, $content);
}
}
}

View File

@ -16,10 +16,12 @@ class UserController extends Controller
{
//
public function userdash(){
$totalData = DB::table('makanans')->count(); // atau model: DataMakanan::count()
$totalData = DB::table('makanans')->count();
$totalKriteria = DB::table('kriterias')->count();
$totalUser = DB::table('users')->count();
$totalUser = DB::table('users')
->join('roles', 'users.role_id', '=', 'roles.id')
->where('roles.name', '=', 'user')
->count();
return view('user.userdash', compact('totalData', 'totalKriteria', 'totalUser'));
}
public function userabout(){
@ -90,22 +92,49 @@ public function userdata(Request $request)
public function userresult()
{
$rekomendasi = \App\Models\RekomendasiAhli::with('komponen', 'waktuMakan')
// Ambil rekomendasi ahli dan group by hari
$rekomendasiAhli = \App\Models\RekomendasiAhli::with(['makanan', 'komponen', 'waktuMakan'])
->get()
->groupBy([
fn($item) => $item->waktu_makan_id,
fn($item) => strtolower($item->komponen->nama),
'hari',
'waktuMakan.nama',
'komponen.nama'
]);
$waktuMakans = \App\Models\WaktuMakan::all()->keyBy('id');
// Ambil data alternatif AHP untuk setiap kombinasi hari, waktu makan, dan komponen
$alternatifAHPData = [];
foreach ($rekomendasiAhli as $hari => $waktuMakans) {
$alternatifAHPData[$hari] = [];
foreach ($waktuMakans as $waktuMakan => $komponens) {
$alternatifAHPData[$hari][$waktuMakan] = [];
foreach ($komponens as $komponen => $makananCollection) {
// Karena groupBy, $makananCollection adalah collection, ambil item pertama
$makananItem = $makananCollection->first();
if ($makananItem) {
// Ambil waktu makan ID dan komponen ID dari item pertama
$waktuMakanId = $makananItem->waktu_makan_id;
$komponenId = $makananItem->komponen_id;
// Ambil alternatif dari tabel rekomendasis
$alternatifGrouped = \App\Models\Rekomendasi::with('makanan')
// Ambil alternatif AHP untuk kombinasi ini
$alternatifAHP = \App\Models\Rekomendasi::with('makanan')
->where('waktu_makan_id', $waktuMakanId)
->where('komponen_id', $komponenId)
->where('nilai_akhir', '>', 0)
->get()
->groupBy(fn($item) => $item->waktu_makan_id . '-' . $item->komponen_id);
->orderBy('nilai_akhir', 'desc')
->take(5)
->get();
$alternatifAHPData[$hari][$waktuMakan][$komponen] = $alternatifAHP;
}
}
}
}
return view('user.userresult', compact('rekomendasi', 'waktuMakans', 'alternatifGrouped'));
return view('user.userresult', compact('rekomendasiAhli', 'alternatifAHPData'));
}

View File

@ -0,0 +1,113 @@
<?php
namespace App\Http\Controllers;
use App\Services\ValidasiRekomendasiService;
use App\Models\ValidasiRekomendasi;
use App\Models\WaktuMakan;
use App\Models\Makanan;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class ValidasiRekomendasiController extends Controller
{
protected $validasiService;
public function __construct(ValidasiRekomendasiService $validasiService)
{
$this->validasiService = $validasiService;
$this->middleware('cekrole:admin');
}
public function index(Request $request)
{
$query = ValidasiRekomendasi::with(['makananPakar', 'waktuMakan', 'komponen']);
// Filter berdasarkan hari
if ($request->has('hari')) {
$query->where('hari', $request->hari);
}
// Filter berdasarkan waktu makan
if ($request->has('waktu_makan_id')) {
$query->where('waktu_makan_id', $request->waktu_makan_id);
}
$validasi = $query->get();
$waktuMakans = WaktuMakan::all();
// Hitung statistik
$total = $validasi->count();
$lebihBaik = $validasi->where('status_kecocokan', 'lebih_baik')->count();
$setara = $validasi->where('status_kecocokan', 'setara')->count();
$lebihBuruk = $validasi->where('status_kecocokan', 'lebih_buruk')->count();
$persentaseCocok = $total > 0
? (($lebihBaik + $setara) / $total) * 100
: 0;
return view('admin.validasi.index', compact(
'validasi',
'waktuMakans',
'total',
'lebihBaik',
'setara',
'lebihBuruk',
'persentaseCocok'
));
}
public function bandingkan()
{
try {
$hasil = $this->validasiService->bandingkanRekomendasi();
return redirect()
->route('validasi.index')
->with('success', 'Validasi berhasil dilakukan. Persentase kecocokan: ' .
number_format($hasil['statistik']['persentase_cocok'], 2) . '%');
} catch (\Exception $e) {
return redirect()
->route('validasi.index')
->with('error', 'Terjadi kesalahan saat melakukan validasi: ' . $e->getMessage());
}
}
public function exportExcel()
{
$validasi = ValidasiRekomendasi::with(['makananPakar', 'waktuMakan', 'komponen'])
->get()
->map(function($item) {
$makananSistemNames = collect($item->makanan_sistem_ids)
->map(function($id) {
$makanan = Makanan::find($id);
return $makanan ? $makanan->nama : 'Makanan tidak ditemukan';
})
->implode(', ');
return [
'Hari' => $item->hari,
'Waktu Makan' => $item->waktuMakan->nama,
'Komponen' => $item->komponen->nama,
'Makanan Pakar' => $item->makananPakar->nama,
'Makanan Sistem' => $makananSistemNames,
'Status Kecocokan' => $this->formatStatusKecocokan($item->status_kecocokan)
];
});
// TODO: Implementasi export Excel menggunakan maatwebsite/excel
// Untuk sementara return view dengan data
return view('admin.validasi.export', compact('validasi'));
}
private function formatStatusKecocokan($status)
{
return match($status) {
'lebih_baik' => '✅ Lebih Baik',
'setara' => '⚠️ Setara',
'lebih_buruk' => '❌ Lebih Buruk',
default => $status
};
}
}

View File

@ -64,5 +64,6 @@ class Kernel extends HttpKernel
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'cekrole' => \App\Http\Middleware\CekRole::class,
'role' => \App\Http\Middleware\Role::class,
];
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class Role
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next, string $role): Response
{
if ($request->user()->role !== $role) {
abort(403, 'Unauthorized action.');
}
return $next($request);
}
}

View File

@ -9,10 +9,15 @@ class BobotKriteria extends Model
{
use HasFactory;
public $timestamps = false;
protected $fillable = ['kriteria_id', 'bobot'];
protected $fillable = ['kriteria_id', 'bobot', 'waktu_makan_id'];
public function kriteria()
{
return $this->belongsTo(Kriteria::class);
}
public function waktuMakan()
{
return $this->belongsTo(WaktuMakan::class);
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class ConsistencyRatio extends Model
{
use HasFactory;
protected $fillable = [
'kriteria_id',
'waktu_makan_id',
'komponen_id',
'user_id',
'nilai_cr',
'tanggal_perhitungan'
];
protected $casts = [
'tanggal_perhitungan' => 'datetime',
'nilai_cr' => 'float'
];
public function kriteria()
{
return $this->belongsTo(Kriteria::class);
}
public function waktuMakan()
{
return $this->belongsTo(WaktuMakan::class);
}
public function komponen()
{
return $this->belongsTo(Komponen::class);
}
public function user()
{
return $this->belongsTo(User::class);
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ConsistencyRatioAlternatif extends Model
{
protected $fillable = [
'kriteria_id',
'waktu_makan_id',
'komponen_id',
'ci',
'cr',
'is_consistent'
];
protected $casts = [
'ci' => 'float',
'cr' => 'float',
'is_consistent' => 'boolean'
];
public function kriteria()
{
return $this->belongsTo(Kriteria::class);
}
public function waktuMakan()
{
return $this->belongsTo(WaktuMakan::class);
}
public function komponen()
{
return $this->belongsTo(Komponen::class);
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ConsistencyRatioCriteria extends Model
{
protected $fillable = [
'ci',
'cr',
'is_consistent',
'waktu_makan_id'
];
protected $casts = [
'ci' => 'float',
'cr' => 'float',
'is_consistent' => 'boolean',
'waktu_makan_id' => 'integer'
];
public function waktuMakan()
{
return $this->belongsTo(WaktuMakan::class);
}
}

View File

@ -9,7 +9,16 @@ class Komponen extends Model
{
use HasFactory;
protected $fillable = ['nama'];
protected $table = 'komponens';
protected $fillable = [
'nama'
];
public function rekomendasiAhli()
{
return $this->hasMany(RekomendasiAhli::class);
}
public function makananKomponenWaktu()
{

View File

@ -17,7 +17,8 @@ class Makanan extends Model
'lemak',
'natrium',
'energi',
'karbohidrat'
'karbohidrat',
'komponen_id'
];
protected $guarded = [];
@ -63,5 +64,8 @@ public function waktuMakans()
->withTimestamps();
}
public function komponen()
{
return $this->belongsTo(Komponen::class);
}
}

View File

@ -11,7 +11,12 @@ class MakananKomponenWaktu extends Model
protected $table = 'makanan_komponen_waktu';
protected $fillable = ['makanan_id', 'komponen_id', 'waktu_makan_id'];
protected $fillable = [
'makanan_id',
'komponen_id',
'waktu_makan_id',
'status'
];
public function makanan()
{

View File

@ -13,6 +13,33 @@ class PerbandinganAlternatif extends Model
'kriteria_id',
'alternatif_id_1',
'alternatif_id_2',
'nilai',
'waktu_makan_id',
'komponen_id',
'nilai'
];
public function kriteria()
{
return $this->belongsTo(Kriteria::class);
}
public function alternatif1()
{
return $this->belongsTo(Makanan::class, 'alternatif_id_1');
}
public function alternatif2()
{
return $this->belongsTo(Makanan::class, 'alternatif_id_2');
}
public function waktuMakan()
{
return $this->belongsTo(WaktuMakan::class);
}
public function komponen()
{
return $this->belongsTo(Komponen::class);
}
}

View File

@ -9,7 +9,7 @@ class PerbandinganKriteria extends Model
{
use HasFactory;
public $timestamps = false;
protected $fillable = ['kriteria_id_1', 'kriteria_id_2', 'nilai'];
protected $fillable = ['kriteria_id_1', 'kriteria_id_2', 'nilai', 'waktu_makan_id'];
public function kriteria1()
{
@ -20,4 +20,8 @@ public function kriteria2()
{
return $this->belongsTo(Kriteria::class, 'kriteria_id_2');
}
public function waktuMakan()
{
return $this->belongsTo(WaktuMakan::class);
}
}

View File

@ -13,26 +13,25 @@ class RekomendasiAhli extends Model
protected $fillable = [
'makanan_id',
'waktu_makan_id',
'komponen_id',
'catatan',
'waktu_makan_id',
'hari'
];
// Relasi ke makanan
public function makanan()
{
return $this->belongsTo(Makanan::class);
return $this->belongsTo(Makanan::class, 'makanan_id');
}
// Relasi ke waktu makan
public function waktuMakan()
{
return $this->belongsTo(WaktuMakan::class);
return $this->belongsTo(WaktuMakan::class, 'waktu_makan_id');
}
// Relasi ke komponen
public function komponen()
{
return $this->belongsTo(Komponen::class);
return $this->belongsTo(Komponen::class, 'komponen_id');
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class SimulasiAhp extends Model
{
use HasFactory;
protected $table = 'simulasi_ahp';
protected $fillable = [
'user_id',
'judul',
'pilihan_makanan_ids',
'hasil_rangking'
];
protected $casts = [
'pilihan_makanan_ids' => 'array',
'hasil_rangking' => 'array',
];
public function user()
{
return $this->belongsTo(User::class);
}
public function pairwise()
{
return $this->hasMany(SimulasiAhpPairwise::class);
}
public function normalisasi()
{
return $this->hasMany(SimulasiAhpNormalisasi::class);
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class SimulasiAhpNormalisasi extends Model
{
use HasFactory;
protected $table = 'simulasi_ahp_normalisasi';
protected $fillable = [
'simulasi_ahp_id',
'makanan_id',
'nilai_normalisasi',
'bobot_akhir'
];
public function simulasi()
{
return $this->belongsTo(SimulasiAhp::class, 'simulasi_ahp_id');
}
public function makanan()
{
return $this->belongsTo(Makanan::class);
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class SimulasiAhpPairwise extends Model
{
use HasFactory;
protected $table = 'simulasi_ahp_pairwise';
protected $fillable = [
'simulasi_ahp_id',
'makanan_1_id',
'makanan_2_id',
'nilai'
];
public function simulasi()
{
return $this->belongsTo(SimulasiAhp::class, 'simulasi_ahp_id');
}
}

View File

@ -9,7 +9,17 @@ class SkorMakanan extends Model
{
use HasFactory;
public $timestamps = false;
protected $fillable = ['makanan_id', 'kriteria_id', 'nilai', 'tanggal_rekomendasi'];
protected $fillable = [
'makanan_id',
'kriteria_id',
'waktu_makan_id',
'komponen_id',
'nilai'
];
protected $casts = [
'nilai' => 'float'
];
public function makanan()
{
@ -20,4 +30,14 @@ public function kriteria()
{
return $this->belongsTo(Kriteria::class);
}
public function waktuMakan()
{
return $this->belongsTo(WaktuMakan::class);
}
public function komponen()
{
return $this->belongsTo(Komponen::class);
}
}

View File

@ -49,4 +49,36 @@ public function role()
{
return $this->belongsTo(Role::class); // Defines the relationship to the Role model
}
/**
* Get the alert setting associated with the user.
*/
public function alertSetting()
{
return $this->hasOne(UserAlertSetting::class);
}
/**
* Get the notifications for the user.
*/
public function notifications()
{
return $this->hasMany(UserNotification::class);
}
/**
* Get unread notifications for the user.
*/
public function unreadNotifications()
{
return $this->notifications()->unread();
}
/**
* Get read notifications for the user.
*/
public function readNotifications()
{
return $this->notifications()->read();
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class UserAlertSetting extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'is_enabled',
'alert_time',
'enabled_waktu_makan_ids'
];
protected $casts = [
'is_enabled' => 'boolean',
'alert_time' => 'datetime',
'enabled_waktu_makan_ids' => 'array'
];
public function user()
{
return $this->belongsTo(User::class);
}
public function getEnabledWaktuMakanAttribute()
{
if (!$this->enabled_waktu_makan_ids) {
return WaktuMakan::where('is_active', true)->get();
}
return WaktuMakan::whereIn('id', $this->enabled_waktu_makan_ids)
->where('is_active', true)
->get();
}
}

View File

@ -0,0 +1,69 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class UserNotification extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'waktu_makan_id',
'title',
'message',
'type',
'is_read',
'read_at',
'data'
];
protected $casts = [
'is_read' => 'boolean',
'read_at' => 'datetime',
'data' => 'array'
];
public function user()
{
return $this->belongsTo(User::class);
}
public function waktuMakan()
{
return $this->belongsTo(WaktuMakan::class);
}
public function scopeUnread($query)
{
return $query->where('is_read', false);
}
public function scopeRead($query)
{
return $query->where('is_read', true);
}
public function scopeByType($query, $type)
{
return $query->where('type', $type);
}
public function markAsRead()
{
$this->update([
'is_read' => true,
'read_at' => now()
]);
}
public function markAsUnread()
{
$this->update([
'is_read' => false,
'read_at' => null
]);
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class ValidasiRekomendasi extends Model
{
use HasFactory;
protected $table = 'validasi_rekomendasi';
protected $fillable = [
'hari',
'waktu_makan_id',
'komponen_id',
'makanan_pakar_id',
'makanan_sistem_ids',
'status_kecocokan'
];
protected $casts = [
'makanan_sistem_ids' => 'array',
];
// Relasi ke makanan
public function makananPakar()
{
return $this->belongsTo(Makanan::class, 'makanan_pakar_id');
}
public function komponen()
{
return $this->belongsTo(Komponen::class, 'komponen_id');
}
public function waktuMakan()
{
return $this->belongsTo(WaktuMakan::class, 'waktu_makan_id');
}
}

View File

@ -4,6 +4,7 @@
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Carbon\Carbon;
class WaktuMakan extends Model
{
@ -11,7 +12,22 @@ class WaktuMakan extends Model
protected $table = 'waktu_makans'; // custom table name
protected $fillable = ['nama', 'keterangan'];
protected $fillable = [
'nama',
'keterangan',
'is_active',
'alert_text'
];
protected $casts = [
'keterangan' => 'string',
'is_active' => 'boolean',
];
public function rekomendasiAhli()
{
return $this->hasMany(RekomendasiAhli::class);
}
public function makananKomponenWaktu()
{
@ -24,4 +40,83 @@ public function makanans()
->withPivot('komponen_id')
->withTimestamps();
}
public function komponens()
{
return $this->belongsToMany(Komponen::class, 'makanan_komponen_waktu', 'waktu_makan_id', 'komponen_id')
->distinct();
}
public function rekomendasis()
{
return $this->hasMany(Rekomendasi::class, 'waktu_makan_id');
}
public function consistencyRatios()
{
return $this->hasMany(ConsistencyRatio::class);
}
public function getLatestConsistencyRatioAttribute()
{
return $this->consistencyRatios()
->where('user_id', auth()->id())
->orderBy('tanggal_perhitungan', 'desc')
->first();
}
/**
* Get waktu makan dalam timezone yang sesuai
*/
public function getWaktuDalamTimezone($timezone = null)
{
$timezone = $timezone ?: config('app.timezone');
// Parse waktu dari database (format time)
$waktu = Carbon::createFromFormat('H:i:s', $this->keterangan);
// Set timezone
$waktu->setTimezone($timezone);
return $waktu;
}
/**
* Get waktu makan yang sudah disesuaikan dengan timezone saat ini
*/
public function getWaktuSekarang()
{
return $this->getWaktuDalamTimezone(config('app.timezone'));
}
/**
* Check apakah sekarang adalah waktu makan ini
*/
public function isWaktuMakanSekarang($tolerance = 30)
{
$waktuMakan = $this->getWaktuSekarang();
$waktuSekarang = Carbon::now(config('app.timezone'));
$selisih = abs($waktuMakan->diffInMinutes($waktuSekarang));
return $selisih <= $tolerance;
}
/**
* Get waktu makan dalam format yang mudah dibaca
*/
public function getWaktuFormatted($timezone = null)
{
$waktu = $this->getWaktuDalamTimezone($timezone);
return $waktu->format('H:i');
}
/**
* Get waktu makan dengan AM/PM
*/
public function getWaktuAMPM($timezone = null)
{
$waktu = $this->getWaktuDalamTimezone($timezone);
return $waktu->format('h:i A');
}
}

View File

@ -3,6 +3,7 @@
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Pagination\Paginator;
class AppServiceProvider extends ServiceProvider
{
@ -19,6 +20,7 @@ public function register(): void
*/
public function boot(): void
{
//
Paginator::defaultView('vendor.pagination.custom');
Paginator::defaultSimpleView('vendor.pagination.custom');
}
}

View File

@ -0,0 +1,142 @@
<?php
namespace App\Services;
use App\Models\User;
use App\Models\UserNotification;
use App\Models\WaktuMakan;
use Carbon\Carbon;
class NotificationService
{
/**
* Kirim notifikasi waktu makan
*/
public static function sendMealTimeNotification($userId, $waktuMakanId)
{
$user = User::find($userId);
$waktuMakan = WaktuMakan::find($waktuMakanId);
if (!$user || !$waktuMakan) {
return false;
}
// Cek apakah sudah ada notifikasi untuk waktu makan ini hari ini
$existingNotification = UserNotification::where('user_id', $userId)
->where('waktu_makan_id', $waktuMakanId)
->whereDate('created_at', today())
->first();
if ($existingNotification) {
return false; // Sudah ada notifikasi hari ini
}
return UserNotification::create([
'user_id' => $userId,
'waktu_makan_id' => $waktuMakanId,
'title' => "Waktu Makan {$waktuMakan->nama}",
'message' => $waktuMakan->alert_text ?? "Waktunya makan {$waktuMakan->nama}!",
'type' => 'reminder',
'data' => [
'waktu_makan' => $waktuMakan->nama,
'waktu' => $waktuMakan->keterangan
]
]);
}
/**
* Kirim notifikasi simulasi selesai
*/
public static function sendSimulationCompletedNotification($userId, $data = [])
{
return UserNotification::create([
'user_id' => $userId,
'title' => 'Simulasi AHP Selesai',
'message' => 'Hasil simulasi AHP Anda sudah siap. Lihat rekomendasi makanan terbaik!',
'type' => 'success',
'data' => $data
]);
}
/**
* Kirim notifikasi umum
*/
public static function sendGeneralNotification($userId, $title, $message, $type = 'info', $data = [])
{
return UserNotification::create([
'user_id' => $userId,
'title' => $title,
'message' => $message,
'type' => $type,
'data' => $data
]);
}
/**
* Cek dan kirim notifikasi waktu makan otomatis
*/
public static function checkAndSendMealTimeNotifications()
{
$currentTime = now()->format('H:i:s');
$users = User::whereHas('alertSetting', function($query) {
$query->where('is_enabled', true);
})->get();
foreach ($users as $user) {
$alertSetting = $user->alertSetting;
if (!$alertSetting || !$alertSetting->is_enabled) {
continue;
}
$enabledWaktuMakan = $alertSetting->enabledWaktuMakan;
foreach ($enabledWaktuMakan as $waktuMakan) {
$waktuMakanTime = $waktuMakan->keterangan;
$timeDiff = abs(strtotime($currentTime) - strtotime($waktuMakanTime));
// Kirim notifikasi jika waktu makan dalam rentang 5 menit
if ($timeDiff <= 300) { // 5 menit = 300 detik
self::sendMealTimeNotification($user->id, $waktuMakan->id);
}
}
}
}
/**
* Tandai notifikasi sebagai dibaca
*/
public static function markAsRead($notificationId, $userId)
{
$notification = UserNotification::where('id', $notificationId)
->where('user_id', $userId)
->first();
if ($notification) {
$notification->markAsRead();
return true;
}
return false;
}
/**
* Tandai semua notifikasi sebagai dibaca
*/
public static function markAllAsRead($userId)
{
return UserNotification::where('user_id', $userId)
->where('is_read', false)
->update([
'is_read' => true,
'read_at' => now()
]);
}
/**
* Hapus notifikasi lama (lebih dari 30 hari)
*/
public static function cleanOldNotifications()
{
return UserNotification::where('created_at', '<', now()->subDays(30))->delete();
}
}

View File

@ -0,0 +1,268 @@
<?php
namespace App\Services;
use App\Models\RekomendasiAhli;
use App\Models\ValidasiRekomendasi;
use App\Models\Rekomendasi;
use App\Models\Makanan;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
class ValidasiRekomendasiService
{
/**
* Bandingkan rekomendasi pakar dengan hasil AHP
* @param int $topN Jumlah makanan teratas yang akan dibandingkan (default: 4)
* @return array Hasil validasi dan persentase kecocokan
*/
public function bandingkanRekomendasi(int $topN = 4): array
{
try {
// Ambil semua rekomendasi pakar
$rekomendasiPakar = RekomendasiAhli::with(['makanan', 'waktuMakan', 'komponen'])->get();
\Log::info('Rekomendasi pakar count: ' . $rekomendasiPakar->count());
// Ambil hasil AHP terbaru untuk setiap waktu makan dan komponen
$hasilAHP = $this->ambilHasilAHP($topN);
\Log::info('Hasil AHP count: ' . $hasilAHP->count());
\Log::info('Hasil AHP structure:', $hasilAHP->toArray());
$hasilValidasi = [];
$totalLebihBaik = 0;
$totalSetara = 0;
$totalLebihBuruk = 0;
foreach ($rekomendasiPakar as $pakar) {
// Cari hasil AHP yang sesuai dengan waktu makan dan komponen
$hasilAHPUntukKomponen = $hasilAHP
->where('waktu_makan_id', $pakar->waktu_makan_id)
->where('komponen_id', $pakar->komponen_id)
->first();
if (!$hasilAHPUntukKomponen) {
\Log::info('Tidak ada hasil AHP untuk waktu_makan_id: ' . $pakar->waktu_makan_id . ', komponen_id: ' . $pakar->komponen_id);
continue;
}
$makananPakar = $pakar->makanan;
$makananSistem = collect($hasilAHPUntukKomponen['makanans']);
$makananSistemIds = $makananSistem->pluck('id')->toArray();
\Log::info('Processing pakar:', [
'pakar_id' => $pakar->id,
'makanan_pakar_id' => $pakar->makanan_id,
'makanan_sistem_ids' => $makananSistemIds
]);
// Cek kecocokan
$status = $this->tentukanStatusKecocokan($makananPakar, $makananSistem);
// Update counter
switch ($status) {
case 'lebih_baik':
$totalLebihBaik++;
break;
case 'setara':
$totalSetara++;
break;
case 'lebih_buruk':
$totalLebihBuruk++;
break;
}
// Simpan hasil validasi
$hasilValidasi[] = [
'hari' => $pakar->hari,
'waktu_makan_id' => $pakar->waktu_makan_id,
'komponen_id' => $pakar->komponen_id,
'makanan_pakar_id' => $pakar->makanan_id,
'makanan_sistem_ids' => $makananSistemIds,
'status_kecocokan' => $status
];
}
// Hitung persentase kecocokan
// $total = count($hasilValidasi);
$total = $totalLebihBaik + $totalSetara + $totalLebihBuruk;
$persentaseCocok = $total > 0
? (($totalLebihBaik + $totalSetara) / $total) * 100 : 0;
// Simpan hasil ke database
$this->simpanHasilValidasi($hasilValidasi);
return [
'hasil_validasi' => $hasilValidasi,
'statistik' => [
'total' => $total,
'lebih_baik' => $totalLebihBaik,
'setara' => $totalSetara,
'lebih_buruk' => $totalLebihBuruk,
'persentase_cocok' => round($persentaseCocok, 2)
]
];
} catch (\Exception $e) {
\Log::error('Error in bandingkanRekomendasi: ' . $e->getMessage(), [
'trace' => $e->getTraceAsString()
]);
throw $e;
}
}
/**
* Ambil hasil AHP terbaru untuk setiap waktu makan dan komponen
*/
private function ambilHasilAHP(int $topN): Collection
{
try {
// Ambil data rekomendasi dengan eager loading
$rekomendasis = Rekomendasi::with(['makanan', 'waktuMakan', 'komponen'])
->orderBy('waktu_makan_id')
->orderBy('komponen_id')
->orderBy('nilai_akhir', 'desc')
->get();
\Log::info('Total rekomendasi found: ' . $rekomendasis->count());
// Group by waktu makan dan komponen
$groupedData = $rekomendasis->groupBy(['waktu_makan_id', 'komponen_id']);
// Transform data structure
$transformedData = collect();
foreach ($groupedData as $waktuMakanId => $komponenGroups) {
foreach ($komponenGroups as $komponenId => $items) {
// Ambil top N makanan berdasarkan nilai_akhir
$topMakanans = $items->take($topN)
->map(function($item) {
return [
'id' => $item->makanan_id,
'nama' => $item->makanan->nama,
'nilai_akhir' => $item->nilai_akhir,
'energi' => $item->makanan->energi,
'lemak' => $item->makanan->lemak,
'karbohidrat' => $item->makanan->karbohidrat,
'natrium' => $item->makanan->natrium
];
});
$transformedData->push([
'waktu_makan_id' => $waktuMakanId,
'komponen_id' => $komponenId,
'makanans' => $topMakanans
]);
\Log::info("Added data for waktu_makan_id: $waktuMakanId, komponen_id: $komponenId, makanan_count: " . $topMakanans->count());
}
}
\Log::info('Transformed data count: ' . $transformedData->count());
return $transformedData;
} catch (\Exception $e) {
\Log::error('Error in ambilHasilAHP: ' . $e->getMessage(), [
'trace' => $e->getTraceAsString()
]);
throw $e;
}
}
/**
* Tentukan status kecocokan antara makanan pakar dan sistem
*/
private function tentukanStatusKecocokan($makananPakar, Collection $makananSistem): string
{
$toleransi = 0.10; // 10%
$lebihBaik = 0;
$setara = 0;
$lebihBuruk = 0;
foreach ($makananSistem as $makanan) {
$lemakPakar = $makananPakar->lemak;
$natriumPakar = $makananPakar->natrium;
$lemakAHP = $makanan['lemak'];
$natriumAHP = $makanan['natrium'];
$lemakSelisih = $this->hitungSelisihPersen($lemakPakar, $lemakAHP);
$natriumSelisih = $this->hitungSelisihPersen($natriumPakar, $natriumAHP);
if ($lemakAHP < $lemakPakar && $natriumAHP < $natriumPakar) {
$lebihBaik++;
} elseif ($lemakSelisih <= $toleransi && $natriumSelisih <= $toleransi) {
$setara++;
} else {
$lebihBuruk++;
}
}
// Tentukan status dominan
// Logika khusus: Jika lebih_baik = 1 dan setara = 1, maka dianggap lebih_baik
if ($lebihBaik === 1 && $setara === 1) {
return 'lebih_baik';
}
// Tentukan status dominan (logika umum)
if ($lebihBaik >= max($setara, $lebihBuruk)) {
return 'lebih_baik';
} elseif ($setara >= max($lebihBaik, $lebihBuruk)) {
return 'setara';
} else {
return 'lebih_buruk';
}
}
private function hitungSelisihPersen($nilai1, $nilai2): float
{
if ($nilai1 == 0 && $nilai2 == 0) return 0;
if ($nilai1 == 0 || $nilai2 == 0) return 1; // 100%
return abs($nilai1 - $nilai2) / max($nilai1, $nilai2);
}
/**
* Cek apakah kandungan gizi dua makanan mirip (±10%)
*/
private function isGiziMirip($makanan1, $makanan2): bool
{
$toleransi = 0.10; // 10%
$giziMirip = true;
$giziMirip &= $this->isDalamToleransi($makanan1->energi, $makanan2['energi'], $toleransi);
$giziMirip &= $this->isDalamToleransi($makanan1->lemak, $makanan2['lemak'], $toleransi);
$giziMirip &= $this->isDalamToleransi($makanan1->karbohidrat, $makanan2['karbohidrat'], $toleransi);
$giziMirip &= $this->isDalamToleransi($makanan1->natrium, $makanan2['natrium'], $toleransi);
return $giziMirip;
}
/**
* Cek apakah dua nilai dalam toleransi yang ditentukan
*/
private function isDalamToleransi($nilai1, $nilai2, $toleransi): bool
{
if ($nilai1 == 0 && $nilai2 == 0) return true;
if ($nilai1 == 0 || $nilai2 == 0) return false;
$selisih = abs($nilai1 - $nilai2) / max($nilai1, $nilai2);
return $selisih <= $toleransi;
}
/**
* Simpan hasil validasi ke database
*/
private function simpanHasilValidasi(array $hasilValidasi): void
{
// Hapus data validasi lama
ValidasiRekomendasi::truncate();
// Simpan data baru
foreach ($hasilValidasi as $validasi) {
ValidasiRekomendasi::create($validasi);
}
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace App\Traits;
trait KriteriaTrait
{
/**
* Daftar kriteria yang termasuk cost (semakin kecil semakin baik)
*/
protected function getCostCriteria(): array
{
return ['lemak', 'natrium'];
}
/**
* Daftar kriteria yang termasuk benefit (semakin besar semakin baik)
*/
protected function getBenefitCriteria(): array
{
return ['energi', 'karbohidrat'];
}
/**
* Cek apakah kriteria termasuk cost
*/
protected function isCostCriteria(string $kriteriaNama): bool
{
return in_array(strtolower($kriteriaNama), $this->getCostCriteria());
}
/**
* Cek apakah kriteria termasuk benefit
*/
protected function isBenefitCriteria(string $kriteriaNama): bool
{
return in_array(strtolower($kriteriaNama), $this->getBenefitCriteria());
}
}

View File

@ -6,10 +6,12 @@
"license": "MIT",
"require": {
"php": "^8.1",
"doctrine/dbal": "*",
"guzzlehttp/guzzle": "^7.2",
"laravel/framework": "^10.0",
"laravel/sanctum": "^3.2",
"laravel/tinker": "^2.8"
"laravel/tinker": "^2.8",
"maatwebsite/excel": "^1.1"
},
"require-dev": {
"fakerphp/faker": "^1.9.1",

515
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "bfe12996eeecb6fdc8713a9fd9d431f8",
"content-hash": "4519d1f6308e7ad5cb4d8d14d0f993b5",
"packages": [
{
"name": "brick/math",
@ -210,6 +210,349 @@
},
"time": "2024-07-08T12:26:09+00:00"
},
{
"name": "doctrine/cache",
"version": "2.2.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/cache.git",
"reference": "1ca8f21980e770095a31456042471a57bc4c68fb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/cache/zipball/1ca8f21980e770095a31456042471a57bc4c68fb",
"reference": "1ca8f21980e770095a31456042471a57bc4c68fb",
"shasum": ""
},
"require": {
"php": "~7.1 || ^8.0"
},
"conflict": {
"doctrine/common": ">2.2,<2.4"
},
"require-dev": {
"cache/integration-tests": "dev-master",
"doctrine/coding-standard": "^9",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.5",
"psr/cache": "^1.0 || ^2.0 || ^3.0",
"symfony/cache": "^4.4 || ^5.4 || ^6",
"symfony/var-exporter": "^4.4 || ^5.4 || ^6"
},
"type": "library",
"autoload": {
"psr-4": {
"Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Guilherme Blanco",
"email": "guilhermeblanco@gmail.com"
},
{
"name": "Roman Borschel",
"email": "roman@code-factory.org"
},
{
"name": "Benjamin Eberlei",
"email": "kontakt@beberlei.de"
},
{
"name": "Jonathan Wage",
"email": "jonwage@gmail.com"
},
{
"name": "Johannes Schmitt",
"email": "schmittjoh@gmail.com"
}
],
"description": "PHP Doctrine Cache library is a popular cache implementation that supports many different drivers such as redis, memcache, apc, mongodb and others.",
"homepage": "https://www.doctrine-project.org/projects/cache.html",
"keywords": [
"abstraction",
"apcu",
"cache",
"caching",
"couchdb",
"memcached",
"php",
"redis",
"xcache"
],
"support": {
"issues": "https://github.com/doctrine/cache/issues",
"source": "https://github.com/doctrine/cache/tree/2.2.0"
},
"funding": [
{
"url": "https://www.doctrine-project.org/sponsorship.html",
"type": "custom"
},
{
"url": "https://www.patreon.com/phpdoctrine",
"type": "patreon"
},
{
"url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcache",
"type": "tidelift"
}
],
"time": "2022-05-20T20:07:39+00:00"
},
{
"name": "doctrine/dbal",
"version": "3.9.4",
"source": {
"type": "git",
"url": "https://github.com/doctrine/dbal.git",
"reference": "ec16c82f20be1a7224e65ac67144a29199f87959"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/dbal/zipball/ec16c82f20be1a7224e65ac67144a29199f87959",
"reference": "ec16c82f20be1a7224e65ac67144a29199f87959",
"shasum": ""
},
"require": {
"composer-runtime-api": "^2",
"doctrine/cache": "^1.11|^2.0",
"doctrine/deprecations": "^0.5.3|^1",
"doctrine/event-manager": "^1|^2",
"php": "^7.4 || ^8.0",
"psr/cache": "^1|^2|^3",
"psr/log": "^1|^2|^3"
},
"require-dev": {
"doctrine/coding-standard": "12.0.0",
"fig/log-test": "^1",
"jetbrains/phpstorm-stubs": "2023.1",
"phpstan/phpstan": "2.1.1",
"phpstan/phpstan-strict-rules": "^2",
"phpunit/phpunit": "9.6.22",
"slevomat/coding-standard": "8.13.1",
"squizlabs/php_codesniffer": "3.10.2",
"symfony/cache": "^5.4|^6.0|^7.0",
"symfony/console": "^4.4|^5.4|^6.0|^7.0"
},
"suggest": {
"symfony/console": "For helpful console commands such as SQL execution and import of files."
},
"bin": [
"bin/doctrine-dbal"
],
"type": "library",
"autoload": {
"psr-4": {
"Doctrine\\DBAL\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Guilherme Blanco",
"email": "guilhermeblanco@gmail.com"
},
{
"name": "Roman Borschel",
"email": "roman@code-factory.org"
},
{
"name": "Benjamin Eberlei",
"email": "kontakt@beberlei.de"
},
{
"name": "Jonathan Wage",
"email": "jonwage@gmail.com"
}
],
"description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.",
"homepage": "https://www.doctrine-project.org/projects/dbal.html",
"keywords": [
"abstraction",
"database",
"db2",
"dbal",
"mariadb",
"mssql",
"mysql",
"oci8",
"oracle",
"pdo",
"pgsql",
"postgresql",
"queryobject",
"sasql",
"sql",
"sqlite",
"sqlserver",
"sqlsrv"
],
"support": {
"issues": "https://github.com/doctrine/dbal/issues",
"source": "https://github.com/doctrine/dbal/tree/3.9.4"
},
"funding": [
{
"url": "https://www.doctrine-project.org/sponsorship.html",
"type": "custom"
},
{
"url": "https://www.patreon.com/phpdoctrine",
"type": "patreon"
},
{
"url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdbal",
"type": "tidelift"
}
],
"time": "2025-01-16T08:28:55+00:00"
},
{
"name": "doctrine/deprecations",
"version": "1.1.5",
"source": {
"type": "git",
"url": "https://github.com/doctrine/deprecations.git",
"reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/deprecations/zipball/459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38",
"reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"conflict": {
"phpunit/phpunit": "<=7.5 || >=13"
},
"require-dev": {
"doctrine/coding-standard": "^9 || ^12 || ^13",
"phpstan/phpstan": "1.4.10 || 2.1.11",
"phpstan/phpstan-phpunit": "^1.0 || ^2",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12",
"psr/log": "^1 || ^2 || ^3"
},
"suggest": {
"psr/log": "Allows logging deprecations via PSR-3 logger implementation"
},
"type": "library",
"autoload": {
"psr-4": {
"Doctrine\\Deprecations\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.",
"homepage": "https://www.doctrine-project.org/",
"support": {
"issues": "https://github.com/doctrine/deprecations/issues",
"source": "https://github.com/doctrine/deprecations/tree/1.1.5"
},
"time": "2025-04-07T20:06:18+00:00"
},
{
"name": "doctrine/event-manager",
"version": "2.0.1",
"source": {
"type": "git",
"url": "https://github.com/doctrine/event-manager.git",
"reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/event-manager/zipball/b680156fa328f1dfd874fd48c7026c41570b9c6e",
"reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e",
"shasum": ""
},
"require": {
"php": "^8.1"
},
"conflict": {
"doctrine/common": "<2.9"
},
"require-dev": {
"doctrine/coding-standard": "^12",
"phpstan/phpstan": "^1.8.8",
"phpunit/phpunit": "^10.5",
"vimeo/psalm": "^5.24"
},
"type": "library",
"autoload": {
"psr-4": {
"Doctrine\\Common\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Guilherme Blanco",
"email": "guilhermeblanco@gmail.com"
},
{
"name": "Roman Borschel",
"email": "roman@code-factory.org"
},
{
"name": "Benjamin Eberlei",
"email": "kontakt@beberlei.de"
},
{
"name": "Jonathan Wage",
"email": "jonwage@gmail.com"
},
{
"name": "Johannes Schmitt",
"email": "schmittjoh@gmail.com"
},
{
"name": "Marco Pivetta",
"email": "ocramius@gmail.com"
}
],
"description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.",
"homepage": "https://www.doctrine-project.org/projects/event-manager.html",
"keywords": [
"event",
"event dispatcher",
"event manager",
"event system",
"events"
],
"support": {
"issues": "https://github.com/doctrine/event-manager/issues",
"source": "https://github.com/doctrine/event-manager/tree/2.0.1"
},
"funding": [
{
"url": "https://www.doctrine-project.org/sponsorship.html",
"type": "custom"
},
{
"url": "https://www.patreon.com/phpdoctrine",
"type": "patreon"
},
{
"url": "https://tidelift.com/funding/github/packagist/doctrine%2Fevent-manager",
"type": "tidelift"
}
],
"time": "2024-05-22T20:47:39+00:00"
},
{
"name": "doctrine/inflector",
"version": "2.0.10",
@ -1884,6 +2227,65 @@
],
"time": "2024-01-28T23:22:08+00:00"
},
{
"name": "maatwebsite/excel",
"version": "v1.1.5",
"source": {
"type": "git",
"url": "https://github.com/Maatwebsite/Laravel-Excel.git",
"reference": "0c67aba8387726458d42461eae91a3415593bbc4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Maatwebsite/Laravel-Excel/zipball/0c67aba8387726458d42461eae91a3415593bbc4",
"reference": "0c67aba8387726458d42461eae91a3415593bbc4",
"shasum": ""
},
"require": {
"php": ">=5.3.0",
"phpoffice/phpexcel": "~1.8.0"
},
"require-dev": {
"mockery/mockery": "~0.9",
"orchestra/testbench": "~2.2.0@dev",
"phpunit/phpunit": "~4.0"
},
"type": "library",
"autoload": {
"psr-0": {
"Maatwebsite\\Excel\\": "src/"
},
"classmap": [
"src/Maatwebsite/Excel",
"tests/TestCase.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL"
],
"authors": [
{
"name": "Maatwebsite.nl",
"email": "patrick@maatwebsite.nl"
}
],
"description": "An eloquent way of importing and exporting Excel and CSV in Laravel 4 with the power of PHPExcel",
"keywords": [
"PHPExcel",
"batch",
"csv",
"excel",
"export",
"import",
"laravel"
],
"support": {
"issues": "https://github.com/Maatwebsite/Laravel-Excel/issues",
"source": "https://github.com/Maatwebsite/Laravel-Excel/tree/master"
},
"time": "2014-07-10T09:06:07+00:00"
},
{
"name": "monolog/monolog",
"version": "3.7.0",
@ -2384,6 +2786,68 @@
],
"time": "2023-02-08T01:06:31+00:00"
},
{
"name": "phpoffice/phpexcel",
"version": "1.8.1",
"source": {
"type": "git",
"url": "https://github.com/PHPOffice/PHPExcel.git",
"reference": "372c7cbb695a6f6f1e62649381aeaa37e7e70b32"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPOffice/PHPExcel/zipball/372c7cbb695a6f6f1e62649381aeaa37e7e70b32",
"reference": "372c7cbb695a6f6f1e62649381aeaa37e7e70b32",
"shasum": ""
},
"require": {
"ext-xml": "*",
"ext-xmlwriter": "*",
"php": ">=5.2.0"
},
"type": "library",
"autoload": {
"psr-0": {
"PHPExcel": "Classes/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL"
],
"authors": [
{
"name": "Maarten Balliauw",
"homepage": "http://blog.maartenballiauw.be"
},
{
"name": "Mark Baker"
},
{
"name": "Franck Lefevre",
"homepage": "http://blog.rootslabs.net"
},
{
"name": "Erik Tilt"
}
],
"description": "PHPExcel - OpenXML - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine",
"homepage": "http://phpexcel.codeplex.com",
"keywords": [
"OpenXML",
"excel",
"php",
"spreadsheet",
"xls",
"xlsx"
],
"support": {
"issues": "https://github.com/PHPOffice/PHPExcel/issues",
"source": "https://github.com/PHPOffice/PHPExcel/tree/master"
},
"abandoned": "phpoffice/phpspreadsheet",
"time": "2015-05-01T07:00:55+00:00"
},
{
"name": "phpoption/phpoption",
"version": "1.9.3",
@ -2459,6 +2923,55 @@
],
"time": "2024-07-20T21:41:07+00:00"
},
{
"name": "psr/cache",
"version": "3.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/cache.git",
"reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf",
"reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf",
"shasum": ""
},
"require": {
"php": ">=8.0.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Cache\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for caching libraries",
"keywords": [
"cache",
"psr",
"psr-6"
],
"support": {
"source": "https://github.com/php-fig/cache/tree/3.0.0"
},
"time": "2021-02-03T23:26:27+00:00"
},
{
"name": "psr/clock",
"version": "1.0.0",

View File

@ -69,7 +69,7 @@
|
*/
'timezone' => 'UTC',
'timezone' => 'Asia/Jakarta',
/*
|--------------------------------------------------------------------------

View File

@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::create('user_notifications', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->foreignId('waktu_makan_id')->nullable()->constrained('waktu_makans')->onDelete('cascade');
$table->string('title');
$table->text('message');
$table->enum('type', ['alert', 'reminder', 'info', 'success', 'warning'])->default('info');
$table->boolean('is_read')->default(false);
$table->timestamp('read_at')->nullable();
$table->json('data')->nullable(); // Data tambahan dalam format JSON
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('user_notifications');
}
};

View File

@ -0,0 +1,24 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::create('consistency_ratio_criterias', function (Blueprint $table) {
$table->id();
$table->float('ci')->comment('Consistency Index');
$table->float('cr')->comment('Consistency Ratio');
$table->boolean('is_consistent')->default(false);
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('consistency_ratio_criterias');
}
};

View File

@ -0,0 +1,95 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
// Add timestamps to bobot_kriterias
Schema::table('bobot_kriterias', function (Blueprint $table) {
if (!Schema::hasColumn('bobot_kriterias', 'created_at')) {
$table->timestamp('created_at')->nullable();
}
if (!Schema::hasColumn('bobot_kriterias', 'updated_at')) {
$table->timestamp('updated_at')->nullable();
}
});
// Add timestamps to perbandingan_kriterias
Schema::table('perbandingan_kriterias', function (Blueprint $table) {
if (!Schema::hasColumn('perbandingan_kriterias', 'created_at')) {
$table->timestamp('created_at')->nullable();
}
if (!Schema::hasColumn('perbandingan_kriterias', 'updated_at')) {
$table->timestamp('updated_at')->nullable();
}
});
// Add timestamps to perbandingan_alternatifs
Schema::table('perbandingan_alternatifs', function (Blueprint $table) {
if (!Schema::hasColumn('perbandingan_alternatifs', 'created_at')) {
$table->timestamp('created_at')->nullable();
}
if (!Schema::hasColumn('perbandingan_alternatifs', 'updated_at')) {
$table->timestamp('updated_at')->nullable();
}
});
// Add timestamps to consistency_ratios
Schema::table('consistency_ratios', function (Blueprint $table) {
if (!Schema::hasColumn('consistency_ratios', 'created_at')) {
$table->timestamp('created_at')->nullable();
}
if (!Schema::hasColumn('consistency_ratios', 'updated_at')) {
$table->timestamp('updated_at')->nullable();
}
});
// Add timestamps to skor_makanans
Schema::table('skor_makanans', function (Blueprint $table) {
if (!Schema::hasColumn('skor_makanans', 'created_at')) {
$table->timestamp('created_at')->nullable();
}
if (!Schema::hasColumn('skor_makanans', 'updated_at')) {
$table->timestamp('updated_at')->nullable();
}
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
// Remove timestamps from bobot_kriterias
Schema::table('bobot_kriterias', function (Blueprint $table) {
$table->dropColumn(['created_at', 'updated_at']);
});
// Remove timestamps from perbandingan_kriterias
Schema::table('perbandingan_kriterias', function (Blueprint $table) {
$table->dropColumn(['created_at', 'updated_at']);
});
// Remove timestamps from perbandingan_alternatifs
Schema::table('perbandingan_alternatifs', function (Blueprint $table) {
$table->dropColumn(['created_at', 'updated_at']);
});
// Remove timestamps from consistency_ratios
Schema::table('consistency_ratios', function (Blueprint $table) {
$table->dropColumn(['created_at', 'updated_at']);
});
// Remove timestamps from skor_makanans
Schema::table('skor_makanans', function (Blueprint $table) {
$table->dropColumn(['created_at', 'updated_at']);
});
}
};

View File

@ -0,0 +1,67 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('perbandingan_alternatifs', function (Blueprint $table) {
$table->unsignedBigInteger('waktu_makan_id')->after('kriteria_id')->nullable();
$table->unsignedBigInteger('komponen_id')->after('waktu_makan_id')->nullable();
$table->foreign('waktu_makan_id')->references('id')->on('waktu_makans')->onDelete('set null');
$table->foreign('komponen_id')->references('id')->on('komponens')->onDelete('set null');
});
Schema::table('skor_makanans', function (Blueprint $table) {
$table->unsignedBigInteger('waktu_makan_id')->after('kriteria_id')->nullable();
$table->unsignedBigInteger('komponen_id')->after('waktu_makan_id')->nullable();
$table->foreign('waktu_makan_id')->references('id')->on('waktu_makans')->onDelete('set null');
$table->foreign('komponen_id')->references('id')->on('komponens')->onDelete('set null');
});
if (!Schema::hasTable('consistency_ratios')) {
Schema::create('consistency_ratios', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('kriteria_id');
$table->unsignedBigInteger('komponen_id');
$table->unsignedBigInteger('waktu_makan_id');
$table->float('nilai');
$table->date('tanggal');
$table->string('status');
$table->timestamps();
$table->foreign('kriteria_id')->references('id')->on('kriterias')->onDelete('cascade');
$table->foreign('komponen_id')->references('id')->on('komponens')->onDelete('cascade');
$table->foreign('waktu_makan_id')->references('id')->on('waktu_makans')->onDelete('cascade');
});
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('perbandingan_alternatifs', function (Blueprint $table) {
$table->dropForeign(['waktu_makan_id']);
$table->dropForeign(['komponen_id']);
$table->dropColumn(['waktu_makan_id', 'komponen_id']);
});
Schema::table('skor_makanans', function (Blueprint $table) {
$table->dropForeign(['waktu_makan_id']);
$table->dropForeign(['komponen_id']);
$table->dropColumn(['waktu_makan_id', 'komponen_id']);
});
Schema::dropIfExists('consistency_ratios');
}
};

View File

@ -0,0 +1,53 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('consistency_ratios', function (Blueprint $table) {
// Tambahkan foreign key constraints
if (!Schema::hasColumn('consistency_ratios', 'waktu_makan_id')) {
$table->unsignedBigInteger('waktu_makan_id')->nullable()->after('kriteria_id');
}
if (!Schema::hasColumn('consistency_ratios', 'komponen_id')) {
$table->unsignedBigInteger('komponen_id')->nullable()->after('waktu_makan_id');
}
// Tambahkan foreign key constraints jika belum ada
$foreignKeys = Schema::getConnection()->getDoctrineSchemaManager()->listTableForeignKeys('consistency_ratios');
$foreignKeyNames = array_map(function($fk) { return $fk->getName(); }, $foreignKeys);
if (!in_array('consistency_ratios_waktu_makan_id_foreign', $foreignKeyNames)) {
$table->foreign('waktu_makan_id')->references('id')->on('waktu_makans')->onDelete('set null');
}
if (!in_array('consistency_ratios_komponen_id_foreign', $foreignKeyNames)) {
$table->foreign('komponen_id')->references('id')->on('komponens')->onDelete('set null');
}
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('consistency_ratios', function (Blueprint $table) {
// Drop foreign keys jika ada
if (Schema::hasColumn('consistency_ratios', 'waktu_makan_id')) {
$table->dropForeign(['waktu_makan_id']);
$table->dropColumn('waktu_makan_id');
}
if (Schema::hasColumn('consistency_ratios', 'komponen_id')) {
$table->dropForeign(['komponen_id']);
$table->dropColumn('komponen_id');
}
});
}
};

View File

@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::dropIfExists('consistency_ratios');
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::create('consistency_ratios', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('kriteria_id');
$table->unsignedBigInteger('komponen_id')->nullable();
$table->unsignedBigInteger('waktu_makan_id')->nullable();
$table->float('nilai');
$table->date('tanggal');
$table->string('status');
$table->timestamps();
$table->foreign('kriteria_id')->references('id')->on('kriterias')->onDelete('cascade');
$table->foreign('komponen_id')->references('id')->on('komponens')->onDelete('set null');
$table->foreign('waktu_makan_id')->references('id')->on('waktu_makans')->onDelete('set null');
});
}
};

View File

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('consistency_ratios', function (Blueprint $table) {
// Tambah kolom kriteria_id jika belum ada
if (!Schema::hasColumn('consistency_ratios', 'kriteria_id')) {
$table->unsignedBigInteger('kriteria_id')->after('id')->nullable();
$table->foreign('kriteria_id')->references('id')->on('kriterias')->onDelete('cascade');
}
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('consistency_ratios', function (Blueprint $table) {
// Hapus foreign key dan kolom jika ada
if (Schema::hasColumn('consistency_ratios', 'kriteria_id')) {
$table->dropForeign(['kriteria_id']);
$table->dropColumn('kriteria_id');
}
});
}
};

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('makanan_komponen_waktu', function (Blueprint $table) {
$table->boolean('status')->default(true)->after('waktu_makan_id');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('makanan_komponen_waktu', function (Blueprint $table) {
$table->dropColumn('status');
});
}
};

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('makanan_komponen_waktu', function (Blueprint $table) {
$table->unique(['makanan_id', 'komponen_id', 'waktu_makan_id'], 'unique_makanan_komponen_waktu');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('makanan_komponen_waktu', function (Blueprint $table) {
$table->dropUnique('unique_makanan_komponen_waktu');
});
}
};

View File

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::create('consistency_ratio_alternatifs', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('kriteria_id');
$table->unsignedBigInteger('waktu_makan_id');
$table->unsignedBigInteger('komponen_id');
$table->double('ci', 8, 4)->comment('Consistency Index');
$table->double('cr', 8, 4)->comment('Consistency Ratio');
$table->boolean('is_consistent')->default(false);
$table->timestamps();
$table->foreign('kriteria_id')->references('id')->on('kriterias')->onDelete('cascade');
$table->foreign('waktu_makan_id')->references('id')->on('waktu_makans')->onDelete('cascade');
$table->foreign('komponen_id')->references('id')->on('komponens')->onDelete('cascade');
});
}
public function down()
{
Schema::dropIfExists('consistency_ratio_alternatifs');
}
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

BIN
public/audio/default.mp3 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@

View File

@ -1,5 +1,7 @@
<?php
ini_set('memory_limit', '1G');
use Illuminate\Contracts\Http\Kernel;
use Illuminate\Http\Request;

142
public/sw.js Normal file
View File

@ -0,0 +1,142 @@
// Service Worker untuk HeartChoice Notifications
const CACHE_NAME = 'heartchoice-v1';
const urlsToCache = [
'/',
'/audio/notification.mp3',
'/audio/default.mp3',
'/logo/baru/dutdut.png'
];
// Install event
self.addEventListener('install', event => {
console.log('Service Worker installing...');
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
console.log('Opened cache');
return cache.addAll(urlsToCache);
})
.catch(error => {
console.log('Cache installation failed:', error);
})
);
});
// Fetch event
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
// Return cached version or fetch from network
return response || fetch(event.request);
})
.catch(error => {
console.log('Fetch failed:', error);
return fetch(event.request);
})
);
});
// Push event untuk notifikasi background
self.addEventListener('push', event => {
console.log('Push event received:', event);
let notificationData = {
title: 'HeartChoice',
body: 'Anda memiliki notifikasi baru!',
icon: '/logo/baru/dutdut.png',
badge: '/logo/baru/dutdut.png',
tag: 'heartchoice-notification',
requireInteraction: false,
silent: false,
vibrate: [200, 100, 200],
data: {
url: '/user/notifications'
}
};
// Jika ada data dari push message
if (event.data) {
try {
const data = event.data.json();
notificationData = {
...notificationData,
...data
};
} catch (e) {
console.log('Error parsing push data:', e);
}
}
event.waitUntil(
self.registration.showNotification(notificationData.title, notificationData)
);
});
// Notification click event
self.addEventListener('notificationclick', event => {
console.log('Notification clicked:', event);
event.notification.close();
// Focus ke window yang ada atau buat window baru
event.waitUntil(
clients.matchAll({ type: 'window' })
.then(clientList => {
// Cari window yang sudah terbuka
for (let client of clientList) {
if (client.url.includes('/user/notifications') && 'focus' in client) {
return client.focus();
}
}
// Jika tidak ada window yang terbuka, buka window baru
if (clients.openWindow) {
return clients.openWindow('/user/notifications');
}
})
);
});
// Notification close event
self.addEventListener('notificationclose', event => {
console.log('Notification closed:', event);
});
// Background sync (untuk fitur offline)
self.addEventListener('sync', event => {
console.log('Background sync:', event);
if (event.tag === 'background-sync') {
event.waitUntil(
// Lakukan sinkronisasi data
console.log('Performing background sync...')
);
}
});
// Message event untuk komunikasi dengan main thread
self.addEventListener('message', event => {
console.log('Service Worker received message:', event.data);
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
// Handle show notification message
if (event.data && event.data.type === 'SHOW_NOTIFICATION') {
const notificationData = event.data.notification;
event.waitUntil(
self.registration.showNotification(notificationData.title, notificationData)
.then(() => {
console.log('Background notification shown successfully');
})
.catch(error => {
console.error('Error showing background notification:', error);
})
);
}
});
console.log('Service Worker loaded successfully');

View File

@ -1,372 +1,436 @@
@extends('layout.app')
@section('title', 'Admin Dashboard')
@include('admin.shared.admin-styles')
@section('content')
<div class="pagetitle">
<h1>Dashboard</h1>
<nav>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="index.html">Home</a></li>
<li class="breadcrumb-item active">Dashboard</li>
</ol>
</nav>
</div><!-- End Page Title -->
<section class="section dashboard">
<div class="row">
<!-- Left side columns -->
<div class="col-lg-8">
<div class="row">
<!-- Users Card -->
<!-- Users Card -->
<div class="col-xxl-6 col-md-6">
<div class="card info-card customers-card">
<div class="card-body position-relative">
<div class="d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">Users <span>| Total</span></h5>
<small>
<a href="{{ route('datauser') }}" class="text-primary d-flex align-items-center" style="text-decoration: none;">
<span>Selengkapnya<span> <i class="bi bi-arrow-right-circle ms-1"></i>
</a>
</small>
</div>
<div class="d-flex align-items-center mt-3">
<div class="card-icon rounded-circle d-flex align-items-center justify-content-center">
<i class="bi bi-people"></i>
</div>
<div class="ps-3">
<h6>{{ $userCount }} Users</h6>
</div>
</div>
</div>
</div>
</div>
<!-- Data Makanan Card -->
<div class="col-xxl-6 col-md-6">
<div class="card info-card sales-card">
<div class="card-body position-relative">
<div class="d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">Data Makanan <span>| Total</span></h5>
<small>
<a href="{{ route('makanan') }}" class="text-primary d-flex align-items-center" style="text-decoration: none;">
<span>Selengkapnya<span> <i class="bi bi-arrow-right-circle ms-1"></i>
</a>
</small>
</div>
<div class="d-flex align-items-center mt-3">
<div class="card-icon rounded-circle d-flex align-items-center justify-content-center">
<i class="bi bi-basket"></i>
</div>
<div class="ps-3">
<h6>{{ $makananCount }} Data</h6>
</div>
</div>
</div>
</div>
</div>
<!-- End Data Makanan Card -->
<div class="card shadow-lg border-start border-4 border-primary mb-4">
<div class="card-body">
<h5 class="card-title text-primary fw-bold mb-0 d-flex justify-content-between align-items-center">
📌 Aturan Penilaian <span class="text-muted">| AHP</span>
<button class="btn btn-sm btn-outline-primary" type="button" data-bs-toggle="collapse" data-bs-target="#collapseAHP" aria-expanded="false" aria-controls="collapseAHP">
Lihat Detail
</button>
</h5>
<div class="collapse mt-3" id="collapseAHP">
<p class="fs-6">Sebelum melakukan perbandingan, pahami skala penilaian berikut. Skala ini digunakan untuk menilai tingkat kepentingan antar kriteria maupun alternatif.</p>
<div class="row row-cols-1 row-cols-md-2 g-3 mt-2">
<div class="col">
<div class="d-flex align-items-start">
<span class="badge bg-success rounded-pill me-3">1</span>
<div>
<strong>Equal Importance</strong><br>
Kedua elemen sama pentingnya
</div>
</div>
</div>
<div class="col">
<div class="d-flex align-items-start">
<span class="badge bg-primary rounded-pill me-3">3</span>
<div>
<strong>Moderate Importance</strong><br>
Salah satu elemen sedikit lebih penting
</div>
</div>
</div>
<div class="col">
<div class="d-flex align-items-start">
<span class="badge bg-warning text-dark rounded-pill me-3">5</span>
<div>
<strong>Strong Importance</strong><br>
Salah satu elemen lebih penting secara kuat
</div>
</div>
</div>
<div class="col">
<div class="d-flex align-items-start">
<span class="badge bg-danger rounded-pill me-3">7</span>
<div>
<strong>Very Strong Importance</strong><br>
Salah satu elemen sangat penting
</div>
</div>
</div>
<div class="col">
<div class="d-flex align-items-start">
<span class="badge bg-dark rounded-pill me-3">9</span>
<div>
<strong>Extreme Importance</strong><br>
Salah satu elemen mutlak lebih penting
</div>
</div>
</div>
<div class="col">
<div class="d-flex align-items-start">
<span class="badge bg-secondary rounded-pill me-3">2, 4, 6, 8</span>
<div>
<strong>Intermediate Values</strong><br>
Nilai antara dua pertimbangan yang berdekatan
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="card shadow-lg border-start border-4 border-primary mb-4">
<div class="card-body">
<h5 class="card-title text-primary fw-bold mb-0 d-flex justify-content-between align-items-center">
📋 Langkah-langkah Mengisi Data <span class="text-muted">| Aplikasi</span>
<button class="btn btn-sm btn-outline-primary" type="button" data-bs-toggle="collapse" data-bs-target="#collapseSteps" aria-expanded="false" aria-controls="collapseSteps">
Lihat Detail
</button>
</h5>
<div class="collapse mt-3" id="collapseSteps">
<p class="fs-6">Berikut adalah langkah-langkah yang harus diikuti untuk mengisi data dalam aplikasi ini. Pastikan Anda mengikuti setiap langkah untuk mendapatkan hasil yang tepat.</p>
<div class="row row-cols-1 row-cols-md-2 g-3 mt-2">
<div class="col">
<div class="d-flex align-items-start">
<span class="badge bg-success rounded-pill me-3">1</span>
<div>
<strong>Masukkan Data Makanan</strong><br>
Pilih kategori makanan dan masukkan informasi seperti lemak, natrium, energi, dan karbohidrat.
</div>
</div>
</div>
<div class="col">
<div class="d-flex align-items-start">
<span class="badge bg-primary rounded-pill me-3">2</span>
<div>
<strong>Input Kriteria</strong><br>
Tentukan kriteria yang relevan untuk perbandingan antar makanan, seperti kandungan gizi dan kecocokan dengan kebutuhan diet.
</div>
</div>
</div>
<div class="col">
<div class="d-flex align-items-start">
<span class="badge bg-warning text-dark rounded-pill me-3">3</span>
<div>
<strong>Perbandingan Berpasangan</strong><br>
Bandingkan alternatif makanan menggunakan skala penilaian untuk menentukan prioritas berdasarkan kriteria.
</div>
</div>
</div>
<div class="col">
<div class="d-flex align-items-start">
<span class="badge bg-danger rounded-pill me-3">4</span>
<div>
<strong>Normalisasi Data</strong><br>
Lakukan normalisasi terhadap data yang telah dimasukkan untuk memastikan konsistensi perbandingan antar makanan.
</div>
</div>
</div>
<div class="col">
<div class="d-flex align-items-start">
<span class="badge bg-dark rounded-pill me-3">5</span>
<div>
<strong>Simpan dan Hasilkan Rekomendasi</strong><br>
Setelah data selesai diinput dan dinormalisasi, simpan perubahan dan hasilkan rekomendasi menu makanan berdasarkan analisis AHP.
</div>
</div>
</div>
<div class="col">
<div class="d-flex align-items-start">
<span class="badge bg-secondary rounded-pill me-3">6</span>
<div>
<strong>Evaluasi dan Sesuaikan</strong><br>
Evaluasi rekomendasi yang diberikan dan sesuaikan jika diperlukan berdasarkan kebutuhan diet atau preferensi pengguna.
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Recent Sales -->
<div class="admin-container container-fluid">
<!-- Page Header -->
<div class="page-header animate-fade-in">
<div class="row align-items-center">
<div class="col-12">
<div class="card recent-sales overflow-auto">
<div class="filter">
<a class="icon" href="#" data-bs-toggle="dropdown"><i class="bi bi-three-dots"></i></a>
<ul class="dropdown-menu dropdown-menu-end dropdown-menu-arrow">
<li class="dropdown-header text-start">
<h6>Filter</h6>
</li>
<li><a class="dropdown-item" href="#">Today</a></li>
<li><a class="dropdown-item" href="#">This Month</a></li>
<li><a class="dropdown-item" href="#">This Year</a></li>
</ul>
</div>
<div class="card-body">
<h5 class="card-title">Recent Sales <span>| Today</span></h5>
<table class="table table-borderless datatable">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Customer</th>
<th scope="col">Product</th>
<th scope="col">Price</th>
<th scope="col">Status</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row"><a href="#">#2457</a></th>
<td>Brandon Jacob</td>
<td><a href="#" class="text-primary">At praesentium minu</a></td>
<td>$64</td>
<td><span class="badge bg-success">Approved</span></td>
</tr>
<tr>
<th scope="row"><a href="#">#2147</a></th>
<td>Bridie Kessler</td>
<td><a href="#" class="text-primary">Blanditiis dolor omnis similique</a></td>
<td>$47</td>
<td><span class="badge bg-warning">Pending</span></td>
</tr>
<tr>
<th scope="row"><a href="#">#2049</a></th>
<td>Ashleigh Langosh</td>
<td><a href="#" class="text-primary">At recusandae consectetur</a></td>
<td>$147</td>
<td><span class="badge bg-success">Approved</span></td>
</tr>
<tr>
<th scope="row"><a href="#">#2644</a></th>
<td>Angus Grady</td>
<td><a href="#" class="text-primar">Ut voluptatem id earum et</a></td>
<td>$67</td>
<td><span class="badge bg-danger">Rejected</span></td>
</tr>
<tr>
<th scope="row"><a href="#">#2644</a></th>
<td>Raheem Lehner</td>
<td><a href="#" class="text-primary">Sunt similique distinctio</a></td>
<td>$165</td>
<td><span class="badge bg-success">Approved</span></td>
</tr>
</tbody>
</table>
</div>
</div>
</div><!-- End Recent Sales -->
</div>
</div><!-- End Left side columns -->
<!-- Right side columns -->
<div class="col-lg-4">
<!-- Recent Activity -->
<!-- Website Traffic -->
<div class="card">
<div class="card-body pb-0">
<h5 class="card-title">Rekomendasi Tertinggi</h5>
<div id="trafficChart" style="min-height: 400px;" class="echart"></div>
<script>
document.addEventListener("DOMContentLoaded", () => {
const chart = echarts.init(document.querySelector("#trafficChart"));
const chartData = @json($chartData);
chart.setOption({
tooltip: {
trigger: 'item'
},
legend: {
top: '5%',
left: 'center'
},
series: [{
name: 'Rekomendasi Makanan',
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: '18',
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: chartData
}]
});
});
</script>
<h3 class="mb-2 text-white">
<i class="fas fa-tachometer-alt me-2"></i>Dashboard
</h3>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item active text-white" aria-current="page">Dashboard</li>
</ol>
</nav>
</div>
</div>
</div>
</div>
<!-- Statistics Overview -->
<div class="row mb-4">
<div class="col-xl-3 col-md-6 mb-4">
<div class="admin-card animate-fade-in">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="feature-icon bg-primary bg-gradient">
<i class="fas fa-users"></i>
</div>
<div class="ms-3">
<h6 class="mb-1">Total Users</h6>
<h4 class="mb-0">{{ $userCount ?? 0 }}</h4>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="admin-card animate-fade-in">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="feature-icon bg-success bg-gradient">
<i class="fas fa-utensils"></i>
</div>
<div class="ms-3">
<h6 class="mb-1">Total Foods</h6>
<h4 class="mb-0">{{ $makananCount ?? 0 }}</h4>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="admin-card animate-fade-in">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="feature-icon bg-warning bg-gradient">
<i class="fas fa-clock"></i>
</div>
<div class="ms-3">
<h6 class="mb-1">Meal Times</h6>
<h4 class="mb-0">{{ $waktuMakanCount ?? 0 }}</h4>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="admin-card animate-fade-in">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="feature-icon bg-info bg-gradient">
<i class="fas fa-chart-pie"></i>
</div>
<div class="ms-3">
<h6 class="mb-1">Components</h6>
<h4 class="mb-0">{{ $komponenCount ?? 0 }}</h4>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Features Overview -->
<div class="row mb-4">
<div class="col-12">
<div class="admin-card animate-fade-in">
<div class="card-body">
<h5 class="card-title mb-4">
<i class="fas fa-th-large me-2"></i>Daftar Rekomendasi AHP
</h5>
<div class="row g-4">
@foreach($waktuMakans as $waktuMakan)
@php
$isNewlyCalculated = $waktuMakan->latest_calculation &&
\Carbon\Carbon::parse($waktuMakan->latest_calculation)->isToday();
@endphp
<div class="col-md-6 col-lg-4">
<div class="meal-card {{ $isNewlyCalculated ? 'newly-calculated' : '' }}">
<div class="meal-header {{ $isNewlyCalculated ? 'bg-gradient-success' : 'bg-gradient-primary' }}">
<div class="d-flex justify-content-between align-items-center">
<h6 class="mb-0">
<i class="fas fa-clock me-2"></i>{{ $waktuMakan->nama }}
</h6>
@if($waktuMakan->has_recommendation)
<span class="status-badge bg-light text-dark">
<i class="fas fa-check me-1"></i>Terhitung
</span>
@else
<span class="status-badge bg-warning">
<i class="fas fa-hourglass-half me-1"></i>Pending
</span>
@endif
</div>
</div>
<div class="meal-content">
@if($waktuMakan->latest_calculation)
<div class="info-row">
<i class="fas fa-calendar me-2"></i>
<span>{{ \Carbon\Carbon::parse($waktuMakan->latest_calculation)->format('d M Y H:i') }}</span>
</div>
@endif
</div>
<div class="meal-actions">
@if($waktuMakan->has_recommendation)
<a href="{{ route('rekomendasi.detail', $waktuMakan->id) }}"
class="btn btn-sm btn-primary">
<i class="fas fa-eye me-1"></i>Detail
</a>
@endif
<a href="{{ route('alternatif.pilih', ['waktu_makan' => $waktuMakan->id]) }}"
class="btn btn-sm {{ $waktuMakan->has_recommendation ? 'btn-outline-primary' : 'btn-primary' }}">
<i class="fas fa-calculator me-1"></i>
{{ $waktuMakan->has_recommendation ? 'Hitung Ulang' : 'Hitung' }}
</a>
</div>
</div>
</div>
@endforeach
</div>
</div>
</div>
</div>
</div>
</div><!-- End Right side columns -->
<!-- AHP Guide -->
<div class="row mb-4">
<div class="col-lg-6">
<div class="admin-card animate-fade-in">
<div class="card-body">
<h5 class="card-title mb-4">
<i class="fas fa-scale-balanced me-2"></i>AHP Scale Guide
</h5>
<div class="row g-3">
<div class="col-sm-6">
<div class="d-flex align-items-center p-3 bg-light rounded">
<span class="admin-badge bg-success me-3">1</span>
<div>
<strong>Equal Importance</strong><br>
<small class="text-muted">Kedua elemen sama pentingnya</small>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="d-flex align-items-center p-3 bg-light rounded">
<span class="admin-badge bg-primary me-3">3</span>
<div>
<strong>Moderate</strong><br>
<small class="text-muted">Sedikit lebih penting</small>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="d-flex align-items-center p-3 bg-light rounded">
<span class="admin-badge bg-warning me-3">5</span>
<div>
<strong>Strong</strong><br>
<small class="text-muted">Lebih penting</small>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="d-flex align-items-center p-3 bg-light rounded">
<span class="admin-badge bg-danger me-3">7</span>
<div>
<strong>Very Strong</strong><br>
<small class="text-muted">Sangat penting</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="admin-card animate-fade-in">
<div class="card-body">
<h5 class="card-title mb-4">
<i class="fas fa-list-check me-2"></i>Quick Guide
</h5>
<div class="steps">
<div class="step d-flex align-items-start mb-4">
<span class="admin-badge bg-primary me-3">1</span>
<div>
<h6 class="mb-2">Data Master</h6>
<p class="text-muted mb-0">Input kriteria, komponen, dan waktu makan</p>
</div>
</div>
<div class="step d-flex align-items-start mb-4">
<span class="admin-badge bg-primary me-3">2</span>
<div>
<h6 class="mb-2">Pengaturan</h6>
<p class="text-muted mb-0">Atur relasi antar komponen</p>
</div>
</div>
<div class="step d-flex align-items-start">
<span class="admin-badge bg-primary me-3">3</span>
<div>
<h6 class="mb-2">Analisis</h6>
<p class="text-muted mb-0">Lakukan perhitungan dan review hasil</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Recent Activity -->
<div class="row">
<div class="col-12">
<div class="admin-card animate-fade-in">
<div class="card-body">
<h5 class="card-title mb-4">
<i class="fas fa-history me-2"></i>Recent Activity
</h5>
<div class="table-responsive">
<table class="admin-table">
<thead>
<tr>
<th>Time</th>
<th>User</th>
<th>Activity</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr>
<td>10:30</td>
<td>Admin</td>
<td>Updated food database</td>
<td><span class="admin-badge bg-success">Completed</span></td>
</tr>
<tr>
<td>11:15</td>
<td>User</td>
<td>Generated recommendations</td>
<td><span class="admin-badge bg-warning">Processing</span></td>
</tr>
<tr>
<td>12:00</td>
<td>System</td>
<td>Backup completed</td>
<td><span class="admin-badge bg-info">Info</span></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
@push('styles')
<style>
/* Status Cards */
.status-card {
padding: 1.5rem;
border-radius: 1rem;
color: white;
height: 100%;
position: relative;
overflow: hidden;
transition: all 0.3s ease;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.status-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.15);
}
.status-icon {
font-size: 2rem;
opacity: 0.9;
margin-bottom: 1rem;
}
.status-content {
position: relative;
z-index: 1;
}
.status-title {
font-size: 1rem;
font-weight: 500;
margin-bottom: 0.5rem;
opacity: 0.9;
}
.status-value {
font-size: 2rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.status-footer {
margin-top: 1rem;
}
.status-label {
font-size: 0.875rem;
opacity: 0.8;
}
/* Gradients */
.bg-gradient-primary {
background: linear-gradient(135deg, #2196f3, #1976d2);
}
.bg-gradient-success {
background: linear-gradient(135deg, #4caf50, #388e3c);
}
.bg-gradient-warning {
background: linear-gradient(135deg, #ff9800, #f57c00);
}
/* Responsive Adjustments */
@media (max-width: 768px) {
.status-card {
padding: 1rem;
}
.status-icon {
font-size: 1.5rem;
margin-bottom: 0.75rem;
}
.status-value {
font-size: 1.5rem;
}
}
.meal-card {
background: white;
border-radius: 0.75rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
overflow: hidden;
transition: all 0.3s ease;
}
.meal-card:hover {
transform: translateY(-3px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.meal-header {
padding: 1rem;
color: white;
}
.meal-header h6 {
font-size: 1rem;
font-weight: 500;
}
.status-badge {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
border-radius: 0.5rem;
}
.meal-content {
padding: 1rem;
background: #f8f9fa;
}
.info-row {
display: flex;
align-items: center;
font-size: 0.875rem;
color: #6c757d;
margin-bottom: 0.5rem;
}
.info-row:last-child {
margin-bottom: 0;
}
.meal-actions {
padding: 1rem;
display: flex;
gap: 0.5rem;
justify-content: flex-end;
background: white;
border-top: 1px solid #e9ecef;
}
.newly-calculated {
border: 2px solid #4caf50;
}
/* Responsive Adjustments */
@media (max-width: 768px) {
.meal-header {
padding: 0.75rem;
}
.meal-content, .meal-actions {
padding: 0.75rem;
}
.info-row {
font-size: 0.8125rem;
}
}
</style>
@endpush
@endsection

View File

@ -1,93 +0,0 @@
@extends('layout.app')
@section('content')
<div class="pagetitle">
<h1>Normalisasi Alternatif</h1>
<nav>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}">Home</a></li>
<li class="breadcrumb-item active"><a href="{{ route('alternatif.pilih') }}">Pemilihan Alternatif</a></li>
<li class="breadcrumb-item active"><a href="{{ route('alternatif.perbandingan') }}">Perbandingan Alternatif</a></li>
<li class="breadcrumb-item active"><a href="{{ route('alternatif.normalisasi') }}">Normalisasi Alternatif</a></li>
</ol>
</nav>
</div><!-- End Page Title -->
<section class="section">
<div class="row">
<div class="col-lg-12">
@if(session('success'))
<div class="alert alert-success">
{{ session('success') }}
</div>
@endif
@foreach ($kriterias as $kriteria)
<div class="card mb-4">
<div class="card-body">
<h5 class="card-title">{{ $kriteria->nama }}</h5>
<div class="table-responsive">
<table class="table table-bordered text-center">
<thead>
<tr>
<th>Alternatif</th>
@foreach ($alternatifs as $alt)
<th>{{ $alt->nama }}</th>
@endforeach
<th>Jumlah</th>
</tr>
</thead>
<tbody>
@php
$jumlahKolom = [];
foreach ($alternatifs as $colAlt) {
$jumlahKolom[$colAlt->id] = 0;
}
// hitung jumlah kolom
foreach ($alternatifs as $rowAlt) {
foreach ($alternatifs as $colAlt) {
$jumlahKolom[$colAlt->id] += $matriks[$kriteria->id][$rowAlt->id][$colAlt->id];
}
}
@endphp
@foreach ($alternatifs as $rowAlt)
<tr>
<th>{{ $rowAlt->nama }}</th>
@php $jumlahBaris = 0; @endphp
@foreach ($alternatifs as $colAlt)
@php
$val = $matriks[$kriteria->id][$rowAlt->id][$colAlt->id];
$normalized = $val / $jumlahKolom[$colAlt->id];
$jumlahBaris += $normalized;
@endphp
<td>{{ number_format($normalized, 4) }}</td>
@endforeach
<td><strong>{{ number_format($jumlahBaris, 4) }}</strong></td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
@endforeach
<!-- Form untuk menyimpan hasil normalisasi -->
<form action="{{ route('alternatif.simpanNormalisasi') }}" method="POST">
@csrf
<div class="text-center mb-5">
<button type="submit" class="btn btn-success">Simpan Normalisasi</button>
</div>
</form>
</div>
</div>
</section>
@endsection

View File

@ -1,80 +1,480 @@
@extends('layout.app')
@section('title', 'Perbandingan Alternatif')
@push('styles')
<style>
.card {
border: none;
border-radius: 15px;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
margin-bottom: 2rem;
transition: all 0.3s ease;
}
.card:hover {
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
}
.card-header {
background: linear-gradient(45deg, #0d6efd, #0dcaf0);
color: white;
border: none;
border-radius: 15px 15px 0 0 !important;
padding: 1.5rem;
}
.card-body {
padding: 1.5rem;
}
.table {
margin-bottom: 0;
}
.table th {
background-color: #f8f9fa;
font-weight: 600;
text-align: center;
vertical-align: middle;
padding: 1rem;
}
.table td {
text-align: center;
vertical-align: middle;
padding: 1rem;
}
.table-hover tbody tr:hover {
background-color: rgba(0,0,0,.075);
}
.score-cell {
font-weight: 500;
color: #0d6efd;
}
.highest-score {
background-color: #d1e7dd !important;
color: #0f5132;
font-weight: bold;
}
.alert-info {
background-color: #e8f4f8;
border-color: #d6e9f9;
border-radius: 10px;
}
.komponen-section {
border: none;
border-radius: 15px;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
margin-bottom: 2rem;
transition: all 0.3s ease;
overflow: hidden;
}
.komponen-section:hover {
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
}
.komponen-header {
background: linear-gradient(45deg, #0d6efd, #0dcaf0);
color: white;
padding: 1.5rem;
border-radius: 15px 15px 0 0;
}
.komponen-body {
background: white;
padding: 1.5rem;
border-radius: 0 0 15px 15px;
}
.rekomendasi-section {
background: #d1e7dd;
border-radius: 10px;
padding: 1.5rem;
margin-bottom: 1.5rem;
transition: all 0.3s ease;
}
.rekomendasi-section:hover {
box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.1);
}
.rekomendasi-section h6 {
color: #0f5132;
margin-bottom: 1rem;
display: flex;
align-items: center;
}
.rekomendasi-section h6 i {
margin-right: 0.75rem;
}
.rekomendasi-list {
list-style: none;
padding-left: 0;
margin-bottom: 0;
}
.rekomendasi-list li {
padding: 1rem;
background: #fff;
border-radius: 10px;
margin-bottom: 0.75rem;
transition: all 0.3s ease;
display: flex;
align-items: center;
}
.rekomendasi-list li:hover {
transform: translateY(-2px);
box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.1);
}
.rekomendasi-list li i {
margin-right: 0.75rem;
color: #0f5132;
}
.rekomendasi-list li:last-child {
margin-bottom: 0;
}
.breadcrumb {
background: transparent;
padding: 0;
}
.breadcrumb-item a {
color: #0d6efd;
text-decoration: none;
}
.breadcrumb-item.active {
color: #6c757d;
}
#loadingOverlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 9999;
backdrop-filter: blur(5px);
}
.loading-content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
color: white;
background: rgba(13, 110, 253, 0.9);
padding: 2rem;
border-radius: 15px;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.3);
}
.loading-spinner {
width: 3rem;
height: 3rem;
margin-bottom: 1rem;
}
@media (max-width: 768px) {
.table-responsive {
border-radius: 10px;
border: 1px solid #dee2e6;
}
.card-header, .komponen-header {
padding: 1rem;
}
.card-body, .komponen-body {
padding: 1rem;
}
.table td, .table th {
padding: 0.75rem;
}
.rekomendasi-section {
padding: 1rem;
}
.rekomendasi-list li {
padding: 0.75rem;
}
}
/* Consistency Ratio Cards */
.cr-card {
padding: 1rem;
border-radius: 0.75rem;
color: white;
height: 100%;
transition: all 0.3s ease;
}
.cr-card:hover {
transform: translateY(-3px);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.cr-title {
font-weight: 600;
margin-bottom: 0.5rem;
}
.cr-value {
font-size: 1.1rem;
font-weight: 500;
margin-bottom: 0.5rem;
}
.cr-status {
font-size: 0.9rem;
opacity: 0.9;
}
.bg-success {
background: linear-gradient(135deg, #4caf50, #388e3c);
}
.bg-warning {
background: linear-gradient(135deg, #ff9800, #f57c00);
}
</style>
@endpush
@section('content')
<div class="pagetitle">
<h1>Perbandingan Alternatif</h1>
<nav>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}">Home</a></li>
<li class="breadcrumb-item active"><a href="{{ route('alternatif.pilih') }}">Pemilihan Alternatif</a></li>
<li class="breadcrumb-item active"><a href="{{ route('alternatif.perbandingan') }}">Perbandingan Alternatif</a></li>
</ol>
</nav>
</div><!-- End Page Title -->
<section class="section">
<div class="content-wrapper">
<div class="row">
<div class="col-lg-12">
<div class="col-12">
<!-- Page Title -->
<div class="d-flex justify-content-between align-items-center mb-4">
<h4 class="card-title mb-0">Hasil Normalisasi Kriteria</h4>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{{ route('alternatif.pilih') }}">Pilih Alternatif</a></li>
<li class="breadcrumb-item"><a href="{{ route('alternatif.view') }}">Lihat Alternatif</a></li>
<li class="breadcrumb-item active">Perbandingan</li>
</ol>
</nav>
</div>
@if(session('success'))
<div class="alert alert-success">{{ session('success') }}</div>
@if(session('error'))
<div class="alert alert-danger">
<i class="fas fa-exclamation-circle me-2"></i>
{{ session('error') }}
</div>
@endif
<div class="card">
<div class="card-body">
<h5 class="card-title">Matriks Perbandingan Alternatif</h5>
<div class="alert alert-info mb-4">
<h5 class="alert-heading mb-2">
<i class="fas fa-info-circle me-2"></i>
Informasi Kriteria
</h5>
<ul class="mb-0">
<li>Untuk kriteria cost (Lemak dan Natrium): Nilai yang lebih kecil lebih baik</li>
<li>Untuk kriteria benefit (Energi dan Karbohidrat): Nilai yang lebih besar lebih baik</li>
<li>Total normalisasi untuk setiap kriteria selalu berjumlah 1.0000</li>
</ul>
</div>
<form action="{{ route('alternatif.simpan') }}" method="POST">
@csrf
@php
$alternatifsByKomponen = session('alternatifs_by_komponen');
@endphp
@foreach ($kriterias as $kriteria)
<h6 class="mt-4">{{ $kriteria->nama }}</h6>
@foreach($alternatifsByKomponen as $komponenId => $komponenData)
<div class="komponen-section mb-4">
<div class="komponen-header">
<h5 class="mb-0">
<i class="fas fa-utensils me-2"></i>
{{ $komponenData['nama_komponen'] }}
</h5>
</div>
<div class="komponen-body">
<!-- Consistency Ratio Section -->
<div class="consistency-info mb-4">
<h6 class="section-title mb-3">
<i class="fas fa-check-circle me-2"></i>
Consistency Ratio (CR) per Kriteria
</h6>
<div class="alert alert-info">
<i class="fas fa-info-circle me-2"></i>
Perhitungan CR sementara dinonaktifkan untuk troubleshooting.
</div>
<!-- <div class="row g-3">
@foreach($kriterias as $kriteria)
@php
$crData = \App\Models\ConsistencyRatioAlternatif::where([
'kriteria_id' => $kriteria->id,
'komponen_id' => $komponenId,
'waktu_makan_id' => session('hasil_rekomendasi.waktu_makan_id')
])->first();
@endphp
<div class="col-md-3">
<div class="cr-card {{ $crData && $crData->is_consistent ? 'bg-success' : 'bg-warning' }}">
<div class="cr-title">{{ $kriteria->nama }}</div>
<div class="cr-value">
CR: {{ $crData ? number_format($crData->cr, 4) : 'N/A' }}
</div>
<div class="cr-status">
@if($crData && $crData->is_consistent)
<i class="fas fa-check-circle me-1"></i> Konsisten
@else
<i class="fas fa-exclamation-circle me-1"></i> Perlu Review
@endif
</div>
</div>
</div>
@endforeach
</div> -->
</div>
<!-- Normalization Table -->
<div class="table-responsive">
<table class="table table-bordered text-center">
<table class="table table-bordered table-hover">
<thead>
<tr>
<th>Alternatif</th>
@foreach ($alternatifs as $alt)
<th>{{ $alt->nama }}</th>
@endforeach
<th style="width: 60px">No</th>
<th>Nama Makanan</th>
<th>Lemak (Normalisasi)</th>
<th>Natrium (Normalisasi)</th>
<th>Energi (Normalisasi)</th>
<th>Karbohidrat (Normalisasi)</th>
</tr>
</thead>
<tbody>
@foreach ($alternatifs as $alt1)
<tr>
<th>{{ $alt1->nama }}</th>
@foreach ($alternatifs as $alt2)
<td>
@if ($alt1->id == $alt2->id)
1
@elseif ($alt1->id < $alt2->id)
<input type="number" step="0.01" min="0.01"
name="nilai[{{ $kriteria->id }}][{{ $alt1->id }}][{{ $alt2->id }}]"
class="form-control" required>
@else
-
@endif
</td>
@endforeach
@php
$no = 1;
// Hitung total untuk setiap kriteria
$totalLemakInvers = array_sum(array_column($komponenData['alternatifs'], 'lemak_invers'));
$totalNatriumInvers = array_sum(array_column($komponenData['alternatifs'], 'natrium_invers'));
$totalEnergi = array_sum(array_column($komponenData['alternatifs'], 'energi'));
$totalKarbohidrat = array_sum(array_column($komponenData['alternatifs'], 'karbohidrat'));
@endphp
@foreach($komponenData['alternatifs'] as $alternatif)
<tr>
<td>{{ $no++ }}</td>
<td class="text-start">{{ $alternatif['nama'] }}</td>
<td class="score-cell">{{ number_format($alternatif['lemak_invers'] / $totalLemakInvers, 4) }}</td>
<td class="score-cell">{{ number_format($alternatif['natrium_invers'] / $totalNatriumInvers, 4) }}</td>
<td class="score-cell">{{ number_format($alternatif['energi'] / $totalEnergi, 4) }}</td>
<td class="score-cell">{{ number_format($alternatif['karbohidrat'] / $totalKarbohidrat, 4) }}</td>
</tr>
@endforeach
<tr>
<td colspan="2" class="text-end fw-bold">Total</td>
<td class="fw-bold">1.0000</td>
<td class="fw-bold">1.0000</td>
<td class="fw-bold">1.0000</td>
<td class="fw-bold">1.0000</td>
</tr>
</tbody>
</table>
</div>
@endforeach
<div class="text-end mt-4">
<button type="submit" name="action" value="simpan" class="btn btn-success">Simpan Saja</button>
<button type="submit" name="action" value="lanjut" class="btn btn-primary">Simpan & Lanjut</button>
</div>
</form>
</div>
@endforeach
<!-- Form untuk menyimpan normalisasi -->
<form action="{{ route('alternatif.simpan.normalisasi') }}" method="POST" id="formNormalisasi">
@csrf
<!-- Simpan semua data dalam satu input JSON -->
<input type="hidden" name="normalisasi_data_json" value="{{ json_encode([
'data' => collect($alternatifsByKomponen)->map(function($komponenData, $komponenId) use ($totalLemakInvers, $totalNatriumInvers, $totalEnergi, $totalKarbohidrat) {
return [
'nama_komponen' => $komponenData['nama_komponen'],
'alternatifs' => collect($komponenData['alternatifs'])->map(function($alternatif) use ($totalLemakInvers, $totalNatriumInvers, $totalEnergi, $totalKarbohidrat) {
return [
'id' => $alternatif['id'],
'nama' => $alternatif['nama'],
'lemak_normalized' => $alternatif['lemak_invers'] / $totalLemakInvers,
'natrium_normalized' => $alternatif['natrium_invers'] / $totalNatriumInvers,
'energi_normalized' => $alternatif['energi'] / $totalEnergi,
'karbohidrat_normalized' => $alternatif['karbohidrat'] / $totalKarbohidrat
];
})->values()->all()
];
})->all()
]) }}">
<div class="text-center mt-4 mb-5">
<button type="button" id="btnSimpanNormalisasi" class="btn btn-success btn-lg">
<i class="fas fa-save me-2"></i>Simpan dan Lanjutkan
</button>
</div>
</form>
<!-- Loading Overlay -->
<div id="loadingOverlay">
<div class="loading-content">
<div class="spinner-border text-light loading-spinner" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<h5 class="mb-0">Memproses Data...</h5>
</div>
</div>
</div>
</div>
</section>
</div>
@endsection
@push('scripts')
<script>
$(document).ready(function() {
// Initialize tooltips
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl)
});
// Animate on scroll
AOS.init({
duration: 800,
easing: 'ease-in-out',
once: true,
mirror: false
});
// Handle form submission with confirmation
$('#btnSimpanNormalisasi').on('click', function(e) {
e.preventDefault();
// Konfirmasi sebelum submit
Swal.fire({
title: 'Konfirmasi',
text: 'Apakah Anda yakin data perbandingan ini sudah benar dan ingin disimpan untuk proses normalisasi?',
icon: 'question',
showCancelButton: true,
confirmButtonText: 'Ya, Simpan',
cancelButtonText: 'Batal',
reverseButtons: true
}).then((result) => {
if (result.isConfirmed) {
// Tampilkan loading overlay
$('#loadingOverlay').fadeIn();
// Submit form secara normal
$('#formNormalisasi').submit();
}
});
});
});
</script>
@endpush

View File

@ -1,134 +1,411 @@
@extends('layout.app')
@section('title', 'Pilih Alternatif')
@push('styles')
<style>
.card {
transition: all 0.3s ease;
border: none;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
}
.komponen-card {
border-radius: 15px;
overflow: hidden;
margin-bottom: 2rem;
}
.komponen-card .card-header {
background: linear-gradient(45deg, #0d6efd, #0dcaf0);
padding: 1rem;
border: none;
}
.makanan-card {
border-radius: 10px;
height: 100%;
}
.makanan-card .card-body {
padding: 1.25rem;
}
.custom-control-input:checked ~ .custom-control-label::before {
background-color: #198754;
border-color: #198754;
}
.nutrition-list {
margin-top: 1rem;
padding: 0.75rem;
background: #f8f9fa;
border-radius: 8px;
}
.nutrition-item {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
color: #6c757d;
}
.nutrition-item:last-child {
margin-bottom: 0;
}
.search-box {
position: relative;
}
.search-box .form-control {
padding-left: 2.5rem;
border-radius: 20px;
}
.search-box i {
position: absolute;
left: 1rem;
top: 50%;
transform: translateY(-50%);
color: #6c757d;
}
.filter-section {
background: #fff;
padding: 1.5rem;
border-radius: 15px;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
margin-bottom: 2rem;
}
.rekomendasi-section {
background: #d1e7dd;
border-radius: 10px;
padding: 1rem;
margin-bottom: 1.5rem;
}
.rekomendasi-section h6 {
color: #0f5132;
margin-bottom: 1rem;
}
.rekomendasi-list {
list-style: none;
padding-left: 0;
margin-bottom: 0;
}
.rekomendasi-list li {
padding: 0.5rem;
background: #fff;
border-radius: 5px;
margin-bottom: 0.5rem;
}
.btn-calculate {
padding: 1rem 2rem;
font-weight: 600;
letter-spacing: 0.5px;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
transition: all 0.3s ease;
}
.btn-calculate:hover {
transform: translateY(-2px);
box-shadow: 0 1rem 2rem rgba(0, 0, 0, 0.15);
}
@media (max-width: 768px) {
.col-md-4 {
margin-bottom: 1rem;
}
}
</style>
@endpush
@section('content')
<div class="pagetitle">
<h1>Pilih Alternatif</h1>
<nav>
<ol class="breadcrumb">
<div class="container-fluid">
<div class="row">
<div class="col-12">
<!-- Page Title -->
<div class="d-flex justify-content-between align-items-center mb-4">
<h3 class="font-weight-bold">Pilih Alternatif Makanan</h3>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}">Dashboard</a></li>
<li class="breadcrumb-item active">Pilih Alternatif</li>
</ol>
</nav>
</div>
<section class="section">
<div class="row">
<div class="col-12">
<!-- Validation Alert -->
<div class="alert alert-info alert-dismissible fade show mb-4" role="alert">
<i class="fas fa-info-circle me-2"></i>
<strong>Penting!</strong> Pilih minimal 4 alternatif makanan untuk setiap komponen untuk perbandingan yang optimal.
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<div class="card">
<div class="card-body">
<h5 class="card-title">Pilih Alternatif (6 makanan)</h5>
@if(session('success'))
<div class="alert alert-success">{{ session('success') }}</div>
@endif
{{-- Form Filter & Search --}}
{{-- Input pencarian (lebih pendek) --}}
<form method="GET" class="d-flex align-items-end flex-wrap gap-2 mb-4">
{{-- Input pencarian --}}
<div style="min-width: 100px; max-width: 170px;">
<input type="text" name="q" class="form-control form-control-sm" placeholder="Cari nama makanan..." value="{{ $search }}">
</div>
{{-- Dropdown kategori --}}
<div style="min-width: 150px; max-width: 180px;">
<select name="kategori" class="form-select form-select-sm">
<option value="">Semua Kategori</option>
@foreach($kategoris as $kategori)
<option value="{{ $kategori->id }}" {{ ($kategoriFilter == $kategori->id) ? 'selected' : '' }}>
{{ $kategori->kategori }}
<!-- Filter Section -->
<div class="filter-section">
<form method="GET" action="{{ route('alternatif.pilih') }}" class="mb-4">
<div class="row align-items-end">
<div class="col-md-4">
<div class="form-group">
<label for="waktu_makan_id" class="form-label">Waktu Makan</label>
<select name="waktu_makan_id" id="waktu_makan_id" class="form-select" required>
<option value="">Pilih Waktu Makan</option>
@foreach($waktuMakans as $waktu)
<option value="{{ $waktu->id }}" {{ $waktuMakanId == $waktu->id ? 'selected' : '' }}>
{{ $waktu->nama }}
</option>
@endforeach
</select>
</div>
{{-- Dropdown urutkan --}}
<div style="min-width: 150px;">
<select name="sort" class="form-select form-select-sm">
<option value="">Semua Kriteria</option>
<option value="lemak" {{ $sort == 'lemak' ? 'selected' : '' }}>Lemak</option>
<option value="natrium" {{ $sort == 'natrium' ? 'selected' : '' }}>Natrium</option>
<option value="energi" {{ $sort == 'energi' ? 'selected' : '' }}>Energi</option>
<option value="karbohidrat" {{ $sort == 'karbohidrat' ? 'selected' : '' }}>Karbohidrat</option>
</select>
</div>
{{-- Dropdown order --}}
<div style="min-width: 140px;">
<select name="order" class="form-select form-select-sm">
<option value="desc" {{ $order == 'desc' ? 'selected' : '' }}>Tertinggi</option>
<option value="asc" {{ $order == 'asc' ? 'selected' : '' }}>Terendah</option>
</select>
</div>
{{-- Tombol --}}
<div class="d-flex gap-2" style="min-width: 140px;">
<button type="submit" class="btn btn-primary btn-sm">Terapkan</button>
<a href="{{ route('alternatif.pilih') }}" class="btn btn-secondary btn-sm">Reset</a>
</div>
</form>
{{-- Form Pemilihan Alternatif --}}
<form action="{{ route('alternatif.pilih.simpan') }}" method="POST">
@csrf
<div class="row">
@foreach ($makanans as $makanan)
<div class="col-12 col-sm-6 col-lg-4 mb-4">
<div class="card h-100 border-0 shadow-sm hover-shadow" style="transition: 0.3s;">
<div class="card-body d-flex align-items-start gap-2">
<input
class="form-check-input mt-1 alternatif-checkbox"
type="checkbox"
name="alternatifs[]"
value="{{ $makanan->id }}"
id="alt{{ $makanan->id }}"
style="transform: scale(1.2);"
>
<label class="form-check-label ms-2 w-100" for="alt{{ $makanan->id }}">
<h6 class="fw-bold mb-1">{{ $makanan->nama }}</h6>
<ul class="list-unstyled small text-muted mb-0">
<li><i class="bi bi-droplet-half me-1"></i> Lemak: {{ $makanan->lemak }} g</li>
<li><i class="bi bi-shield-check me-1"></i> Natrium: {{ $makanan->natrium }} mg</li>
<li><i class="bi bi-lightning me-1"></i> Energi: {{ $makanan->energi }} kal</li>
<li><i class="bi bi-activity me-1"></i> Karbohidrat: {{ $makanan->karbohidrat }} g</li>
</ul>
</label>
</div>
</div>
</div>
@endforeach
</div>
<div class="d-flex justify-content-end">
<button type="submit" class="btn btn-success btn-lg px-4 mt-3" id="submitBtn" disabled>
<i class="bi bi-arrow-right-circle-fill me-1"></i> Lanjutkan
<div class="col-md-6">
<div class="form-group search-box">
<label for="q" class="form-label">Cari Makanan</label>
<i class="fas fa-search"></i>
<input type="text" name="q" id="q" class="form-control"
value="{{ $search ?? '' }}" placeholder="Ketik nama makanan...">
</div>
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-primary w-100">
<i class="fas fa-filter me-2"></i>Filter
</button>
</div>
</form>
<hr class="my-4">
</div>
</div>
</form>
</div>
@if($waktuMakanId)
<!-- Form Pilih Alternatif -->
<form action="{{ route('alternatif.pilih.simpan') }}" method="POST" id="formPilihAlternatif">
@csrf
<input type="hidden" name="waktu_makan_id" value="{{ $waktuMakanId }}">
<!-- Komponen Cards -->
@foreach($komponens as $komponen)
<div class="komponen-card">
<div class="card-header d-flex align-items-center justify-content-between">
<h5 class="card-title mb-0 text-white d-flex align-items-center">
<i class="fas fa-utensils me-2"></i>{{ $komponen->nama }}
</h5>
<div class="form-check ms-auto">
<input class="form-check-input pilih-semua"
type="checkbox"
data-komponen-id="{{ $komponen->id }}"
id="pilihSemua{{ $komponen->id }}">
<label class="form-check-label text-white ms-2" for="pilihSemua{{ $komponen->id }}">
<strong>Pilih Semua</strong>
</label>
</div>
</div>
<div class="card-body bg-white">
<!-- Hidden input for komponen_id -->
<input type="hidden" name="komponen_ids[]" value="{{ $komponen->id }}" class="komponen-input">
<!-- Rekomendasi Ahli Section -->
@php
$rekomendasiKomponen = $rekomendasiAhli->where('komponen_id', $komponen->id)
->where('waktu_makan_id', $waktuMakanId);
@endphp
@if($rekomendasiKomponen->isNotEmpty())
<div class="rekomendasi-section">
<h6><i class="fas fa-lightbulb me-2"></i>Rekomendasi Ahli:</h6>
<ul class="rekomendasi-list">
@foreach($rekomendasiKomponen as $rekomendasi)
<li>{{ $rekomendasi->makanan->nama ?? 'Makanan tidak ditemukan' }}</li>
@endforeach
</ul>
</div>
@endif
<!-- Alternatif Makanan Section -->
@php
$makananKomponen = $makanans->where('komponen_id', $komponen->id);
@endphp
@if($makananKomponen->isNotEmpty())
<div class="row">
@foreach($makananKomponen as $makanan)
<div class="col-md-4 mb-3">
<div class="makanan-card card h-100">
<div class="card-body">
<div class="custom-control custom-checkbox">
<input type="checkbox"
class="custom-control-input komponen-{{ $komponen->id }}"
name="alternatifs[]"
value="{{ $makanan->id }}"
id="makanan{{ $makanan->id }}">
<label class="custom-control-label" for="makanan{{ $makanan->id }}">
<h6 class="mb-2">{{ $makanan->nama }}</h6>
</label>
</div>
<div class="nutrition-list">
<div class="nutrition-item">
<span><i class="fas fa-circle-dot me-2"></i>Lemak</span>
<span>{{ $makanan->lemak }}g</span>
</div>
<div class="nutrition-item">
<span><i class="fas fa-circle-dot me-2"></i>Natrium</span>
<span>{{ $makanan->natrium }}mg</span>
</div>
<div class="nutrition-item">
<span><i class="fas fa-circle-dot me-2"></i>Energi</span>
<span>{{ $makanan->energi }}kal</span>
</div>
<div class="nutrition-item">
<span><i class="fas fa-circle-dot me-2"></i>Karbohidrat</span>
<span>{{ $makanan->karbohidrat }}g</span>
</div>
</div>
</div>
</div>
</div>
@endforeach
</div>
@else
<div class="alert alert-warning">
<i class="fas fa-exclamation-triangle me-2"></i>
Tidak ada alternatif makanan yang tersedia untuk komponen ini.
</div>
@endif
</div>
</div>
@endforeach
<div class="text-center mt-4 mb-5">
<button type="submit" class="btn btn-success btn-lg btn-calculate">
<i class="fas fa-calculator me-2"></i>Proses Perhitungan AHP
</button>
</div>
</form>
@else
<div class="alert alert-info">
<i class="fas fa-info-circle me-2"></i>
Silakan pilih Waktu Makan untuk melihat rekomendasi dan alternatif yang tersedia.
</div>
@endif
</div>
</div>
</section>
</div>
@endsection
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function () {
const checkboxes = document.querySelectorAll('.alternatif-checkbox');
const submitBtn = document.getElementById('submitBtn');
const max = 6;
const min = 6;
$(document).ready(function() {
// event handler untuk checkbox pilih semua
$('.pilih-semua').on('change', function () {
let komponenId = $(this).data('komponen-id');
let checked = $(this).is(':checked');
$(`.komponen-${komponenId}`).prop('checked', checked);
updateCounter(komponenId);
updateSubmitButton();
});
// Fungsi untuk menghitung jumlah checkbox yang dicentang per komponen
function getCheckedCountByKomponen(komponenId) {
return $(`.komponen-${komponenId}:checked`).length;
}
// Fungsi untuk update counter di card header
function updateCounter(komponenId) {
const checkedCount = getCheckedCountByKomponen(komponenId);
const cardHeader = $(`.komponen-${komponenId}`).first().closest('.komponen-card').find('.card-title');
const originalText = cardHeader.text().replace(/\(\d+\/4\+\)$/, '').trim();
cardHeader.text(`${originalText} (${checkedCount}/4+)`);
}
// Fungsi untuk memvalidasi form
function validateForm() {
let isValid = true;
let errorMessages = [];
// Cek setiap komponen
$('.komponen-input').each(function() {
const komponenId = $(this).val();
const checkedCount = getCheckedCountByKomponen(komponenId);
if (checkedCount < 4) {
isValid = false;
const komponenName = $(this).closest('.komponen-card').find('.card-title').text().replace(/\(\d+\/4\+\)$/, '').trim();
errorMessages.push(`Pilih minimal 4 alternatif untuk komponen ${komponenName} (saat ini: ${checkedCount})`);
}
});
return {
isValid: isValid,
messages: errorMessages
};
}
function update() {
const checked = document.querySelectorAll('.alternatif-checkbox:checked').length;
submitBtn.disabled = !(checked >= min && checked <= max);
checkboxes.forEach(cb => {
cb.disabled = !cb.checked && checked >= max;
// Event handler untuk checkbox
$('input[type="checkbox"]').on('change', function() {
const komponenId = $(this).attr('class').split(' ').find(cls => cls.startsWith('komponen-'))?.split('-')[1];
if (komponenId) {
updateCounter(komponenId);
updateSubmitButton();
}
});
// Update tombol submit berdasarkan validasi
function updateSubmitButton() {
const validation = validateForm();
const submitBtn = $('.btn-calculate');
if (!validation.isValid) {
submitBtn.prop('disabled', true)
.attr('title', validation.messages.join('\n'));
} else {
submitBtn.prop('disabled', false)
.attr('title', 'Lanjutkan ke perhitungan AHP');
}
}
// Inisialisasi tombol submit dan counter
updateSubmitButton();
// Inisialisasi counter di setiap card header
$('.komponen-input').each(function() {
const komponenId = $(this).val();
updateCounter(komponenId);
});
// Validasi form sebelum submit
$('#formPilihAlternatif').on('submit', function(e) {
const validation = validateForm();
if (!validation.isValid) {
e.preventDefault();
Swal.fire({
title: 'Peringatan',
html: validation.messages.join('<br>'),
icon: 'warning',
confirmButtonText: 'Mengerti'
});
}
checkboxes.forEach(cb => {
cb.addEventListener('change', update);
});
});
// Animasi smooth scroll untuk cards
if (typeof AOS !== 'undefined') {
AOS.init({
duration: 800,
easing: 'ease-in-out',
once: true,
mirror: false
});
}
});
</script>
@endpush

View File

@ -0,0 +1,266 @@
@extends('layout.app')
@section('title', 'Lihat Alternatif Terpilih')
@push('styles')
<style>
.card {
border: none;
border-radius: 15px;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
margin-bottom: 2rem;
transition: all 0.3s ease;
}
.card:hover {
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
}
.card-header {
background: linear-gradient(45deg, #0d6efd, #0dcaf0);
color: white;
border: none;
border-radius: 15px 15px 0 0 !important;
padding: 1.5rem;
}
.card-body {
padding: 1.5rem;
}
.table th {
background-color: #f8f9fa;
font-weight: 600;
text-align: center;
vertical-align: middle;
padding: 1rem;
}
.table td {
text-align: center;
vertical-align: middle;
padding: 1rem;
}
.nutrition-value {
font-weight: 500;
color: #0d6efd;
}
.invers-value {
font-weight: 500;
color: #198754;
}
.btn-next {
padding: 1rem 2rem;
font-weight: 600;
letter-spacing: 0.5px;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
transition: all 0.3s ease;
}
.btn-next:hover {
transform: translateY(-2px);
box-shadow: 0 1rem 2rem rgba(0, 0, 0, 0.15);
}
.table-title {
font-size: 1.1rem;
font-weight: 600;
color: #495057;
margin: 1rem 0;
padding: 1rem;
background: #f8f9fa;
border-radius: 10px;
display: flex;
align-items: center;
}
.table-title i {
margin-right: 0.75rem;
color: #0d6efd;
}
.table-invers .table-title i {
color: #198754;
}
.alert-info {
background-color: #e8f4f8;
border-color: #d6e9f9;
border-radius: 10px;
}
.breadcrumb {
background: transparent;
padding: 0;
}
.breadcrumb-item a {
color: #0d6efd;
text-decoration: none;
}
.breadcrumb-item.active {
color: #6c757d;
}
.table-hover tbody tr:hover {
background-color: rgba(0,0,0,.075);
}
@media (max-width: 768px) {
.table-responsive {
border-radius: 10px;
border: 1px solid #dee2e6;
}
.card-header {
padding: 1rem;
}
.card-body {
padding: 1rem;
}
.table td, .table th {
padding: 0.75rem;
}
}
</style>
@endpush
@section('content')
<div class="content-wrapper">
<div class="row">
<div class="col-12">
<!-- Page Title -->
<div class="d-flex justify-content-between align-items-center mb-4">
<h4 class="card-title mb-0">Alternatif Terpilih</h4>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{{ route('alternatif.pilih') }}">Pilih Alternatif</a></li>
<li class="breadcrumb-item active">Lihat Alternatif</li>
</ol>
</nav>
</div>
@php
$alternatifsByKomponen = session('alternatifs_by_komponen');
$hasilRekomendasi = session('hasil_rekomendasi');
@endphp
@if($alternatifsByKomponen && $hasilRekomendasi)
<!-- Info Section -->
<div class="alert alert-info mb-4">
<h5 class="alert-heading mb-2">
<i class="fas fa-info-circle me-2"></i>
Informasi Pemilihan
</h5>
<p class="mb-2">
Waktu Makan: <strong>{{ $hasilRekomendasi['waktu_makan_nama'] }}</strong><br>
Tanggal: <strong>{{ \Carbon\Carbon::parse($hasilRekomendasi['tanggal_rekomendasi'])->format('d F Y') }}</strong>
</p>
<hr>
<p class="mb-0">
<i class="fas fa-lightbulb me-2"></i>
Nilai invers digunakan untuk kriteria cost (Lemak dan Natrium), dimana semakin kecil nilainya semakin baik.
</p>
</div>
<!-- Alternatif Cards -->
@foreach($alternatifsByKomponen as $komponenId => $komponen)
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-utensils me-2"></i>
{{ $komponen['nama_komponen'] }}
</h5>
</div>
<div class="card-body">
<!-- Normal Values Table -->
<div class="table-normal mb-4">
<div class="table-title">
<i class="fas fa-table"></i>
Nilai Nutrisi
</div>
<div class="table-responsive">
<table class="table table-bordered table-hover">
<thead>
<tr>
<th>Nama Makanan</th>
<th>Lemak (g)</th>
<th>Natrium (mg)</th>
<th>Energi (kal)</th>
<th>Karbohidrat (g)</th>
</tr>
</thead>
<tbody>
@foreach($komponen['alternatifs'] as $alternatif)
<tr>
<td>{{ $alternatif['nama'] }}</td>
<td class="nutrition-value">{{ number_format($alternatif['lemak'], 2) }}</td>
<td class="nutrition-value">{{ number_format($alternatif['natrium'], 2) }}</td>
<td class="nutrition-value">{{ number_format($alternatif['energi'], 2) }}</td>
<td class="nutrition-value">{{ number_format($alternatif['karbohidrat'], 2) }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
<!-- Inverse Values Table -->
<div class="table-invers">
<div class="table-title">
<i class="fas fa-calculator"></i>
Nilai Invers (Cost Criteria)
</div>
<div class="table-responsive">
<table class="table table-bordered table-hover">
<thead>
<tr>
<th>Nama Makanan</th>
<th>Lemak (Invers)</th>
<th>Natrium (Invers)</th>
<th>Energi (kal)</th>
<th>Karbohidrat (g)</th>
</tr>
</thead>
<tbody>
@foreach($komponen['alternatifs'] as $alternatif)
<tr>
<td>{{ $alternatif['nama'] }}</td>
<td class="invers-value">{{ number_format($alternatif['lemak_invers'], 4) }}</td>
<td class="invers-value">{{ number_format($alternatif['natrium_invers'], 4) }}</td>
<td class="nutrition-value">{{ number_format($alternatif['energi'], 2) }}</td>
<td class="nutrition-value">{{ number_format($alternatif['karbohidrat'], 2) }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>
@endforeach
<!-- Next Step Button -->
<div class="text-center mt-4 mb-5">
<a href="{{ route('alternatif.perbandingan') }}" class="btn btn-success btn-lg btn-next">
<i class="fas fa-arrow-right me-2"></i>
Lanjut ke Perbandingan
</a>
</div>
@else
<div class="alert alert-warning">
<i class="fas fa-exclamation-triangle me-2"></i>
Tidak ada data alternatif yang dipilih. Silakan pilih alternatif terlebih dahulu.
</div>
@endif
</div>
</div>
</div>
@endsection

View File

@ -1,65 +1,115 @@
@extends('layout.app')
@section('content')
<div class="pagetitle">
<h1>Data User</h1>
<nav>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}">Home</a></li>
<li class="breadcrumb-item active"><a href="{{ route('datauser') }}"> User</a></li>
</ol>
</nav>
</div><!-- End Page Title -->
<section class="section">
@section('title', 'User Management')
@include('admin.shared.admin-styles')
@section('content')
<div class="admin-container container-fluid">
<!-- Page Header -->
<div class="page-header animate-fade-in">
<div class="row align-items-center">
<div class="col-12">
<h3 class="mb-2 text-white">
<i class="fas fa-users me-2"></i>User Management
</h3>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}" class="text-white-50">Dashboard</a></li>
<li class="breadcrumb-item active text-white" aria-current="page">Users</li>
</ol>
</nav>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-12">
<div class="card">
<div class="col-12">
<div class="admin-card animate-fade-in">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<h5 class="card-title mb-2">Data User</h5>
<div class="text-center">
<a href="{{ route('tambahuser') }}" class="btn btn-primary">Tambah</a>
</div>
<div class="d-flex justify-content-between align-items-center mb-4">
<h5 class="card-title mb-0">
<i class="fas fa-list me-2"></i>User List
</h5>
<a href="{{ route('tambahuser') }}" class="admin-btn btn-primary">
<i class="fas fa-plus me-2"></i>Add New User
</a>
</div>
<!-- Table with stripped rows -->
<table class="table datatable">
<thead>
<tr>
<th class="text-center">No</th>
<th class="text-center">Name</th>
<th class="text-center">Email</th>
<th class="text-center">No telp</th>
<th class="text-center">Password</th>
<th class="text-center">Role</th>
<th class="text-center">Action</th>
</tr>
</thead>
<tbody>
@foreach ($users as $usr)
<tr>
<td class="text-center">{{ $loop->iteration }}</td>
<td class="text-center">{{ $usr->name }}</td>
<td class="text-center">{{ $usr->email }}</td>
<td class="text-center">{{ $usr->no_telp }}</td>
<td class="text-center">****</td> <!-- Tampilkan bintang, bukan password asli -->
<td class="text-center">{{ $usr->role->name }}</td> <!-- Display role name -->
<td class="text-center">
<a href="{{ route('edituser', $usr->id) }}" class="btn btn-sm btn-warning">Edit</a>
<form action="{{ route('deleteuser', $usr->id) }}" method="POST" class="d-inline delete-form">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-sm btn-danger delete-button">Hapus</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
<!-- End Table with stripped rows -->
<div class="table-responsive">
<table class="admin-table">
<thead>
<tr>
<th>No</th>
<th>Name</th>
<th>Email</th>
<th>Phone</th>
<th>Role</th>
<th class="text-center">Actions</th>
</tr>
</thead>
<tbody>
@foreach ($users as $usr)
<tr>
<td>{{ $loop->iteration }}</td>
<td>
<div class="d-flex align-items-center">
{{ $usr->name }}
</div>
</td>
<td>{{ $usr->email }}</td>
<td>{{ $usr->no_telp }}</td>
<td>
<span class="admin-badge bg-info">
{{ $usr->role->name }}
</span>
</td>
<td class="text-center">
<a href="{{ route('edituser', $usr->id) }}"
class="admin-btn btn-warning btn-sm me-2">
<i class="fas fa-edit"></i>
</a>
<form action="{{ route('deleteuser', $usr->id) }}"
method="POST"
class="d-inline delete-form"
onsubmit="return confirm('Are you sure you want to delete this user?');">
@csrf
@method('DELETE')
<button type="submit" class="admin-btn btn-danger btn-sm">
<i class="fas fa-trash"></i>
</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
@endsection
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
const animateElements = document.querySelectorAll('.animate-fade-in');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.opacity = 1;
entry.target.style.transform = 'translateY(0)';
}
});
});
animateElements.forEach(element => {
element.style.opacity = 0;
element.style.transform = 'translateY(20px)';
observer.observe(element);
});
});
</script>
@endpush

View File

@ -1,113 +1,310 @@
@extends('layout.app')
@section('title', 'Edit User')
@include('admin.shared.admin-styles')
@section('content')
<div class="pagetitle">
<h1>Edit Data User</h1>
<nav>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}">Home</a></li>
<li class="breadcrumb-item active"><a href="{{ route('datauser') }}">User</a></li>
<li class="breadcrumb-item active"><a href="{{ route('edituser', $user->id) }}">Edit User</a></li>
</ol>
</nav>
</div><!-- End Page Title -->
<div class="card">
<div class="card-body">
<h5 class="card-title">Edit User</h5>
<!-- General Form Elements -->
<form id="editUserForm" method="POST" action="{{ url("/datauser/{$user->id}") }}">
@method('PUT')
@csrf
<div class="row mb-3">
<label for="inputText" class="col-sm-2 col-form-label">Name</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="name" value="{{ $user->name }}">
</div>
</div>
<div class="row mb-3">
<label for="inputEmail" class="col-sm-2 col-form-label">Email</label>
<div class="col-sm-10">
<input type="email" class="form-control" name="email" value="{{ $user->email }}">
</div>
</div>
<div class="row mb-3">
<label for="inputNumber" class="col-sm-2 col-form-label">No Telp</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="no_telp" value="{{ $user->no_telp }}">
</div>
</div>
{{-- <div class="row mb-3">
<label for="inputPassword" class="col-sm-2 col-form-label">Address</label>
<div class="col-sm-10">
<textarea class="form-control" style="height: 100px" name="address">{{ $admin->address }}</textarea>
</div>
</div> --}}
<div class="row mb-3">
<label for="inputPassword" class="col-sm-2 col-form-label">Password</label>
<div class="col-sm-8">
<input type="password" class="form-control" name="password" value="{{ $user->password }}"
id="passwordField">
</div>
<div class="col-sm-2">
<button type="button" class="btn btn-primary" onclick="togglePasswordVisibility()">Show</button>
</div>
</div>
<div class="row mb-3">
<label for="role" class="col-sm-2 col-form-label">Role</label>
<div class="col-sm-10">
<select id="role" class="form-select" name="role_id">
@foreach ($roles as $id => $name)
<option value="{{ $id }}" {{ $user->role_id == $id ? 'selected' : '' }}>{{ $name }}</option>
@endforeach
</select>
</div>
</div>
<div class="row mb-3">
<div class="col-sm-12 text-center">
<a href="{{ route('datauser') }}" class="btn btn-secondary mx-3">Back</a>
<button type="button" class="btn btn-primary mx-3" onclick="confirmSave()">Save</button>
<!-- Reset button to reset form fields -->
<button type="reset" class="btn btn-secondary">Reset</button>
</div>
</div>
</form><!-- End General Form Elements -->
<div class="admin-container container-fluid">
<!-- Page Header -->
<div class="page-header animate-fade-in">
<div class="row align-items-center">
<div class="col-12">
<h3 class="mb-2 text-white">
<i class="fas fa-user-edit me-2"></i>Edit User
</h3>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}" class="text-white-50">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{{ route('datauser') }}" class="text-white-50">Users</a></li>
<li class="breadcrumb-item active text-white" aria-current="page">Edit User</li>
</ol>
</nav>
</div>
</div>
</div>
<!-- SweetAlert2 -->
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<div class="row">
<div class="col-12">
<div class="admin-card animate-fade-in">
<div class="card-body">
<h5 class="card-title mb-4">
<i class="fas fa-user-cog me-2"></i>User Information
</h5>
<!-- Confirmation Dialog Script -->
<script>
function togglePasswordVisibility() {
var passwordField = document.getElementById("passwordField");
if (passwordField.type === "password") {
passwordField.type = "text";
document.querySelector('button[onclick="togglePasswordVisibility()"]').textContent = "Hide";
} else {
passwordField.type = "password";
document.querySelector('button[onclick="togglePasswordVisibility()"]').textContent = "Show";
}
}
<form id="editUserForm" method="POST" action="{{ url("/datauser/{$user->id}") }}">
@method('PUT')
@csrf
<div class="row g-4">
<div class="col-md-6">
<div class="admin-form-group">
<label class="admin-form-label">
<i class="fas fa-user me-2"></i>Name
</label>
<input type="text"
class="admin-form-control"
name="name"
value="{{ $user->name }}"
required>
</div>
</div>
<div class="col-md-6">
<div class="admin-form-group">
<label class="admin-form-label">
<i class="fas fa-envelope me-2"></i>Email
</label>
<input type="email"
class="admin-form-control"
name="email"
value="{{ $user->email }}"
required>
</div>
</div>
<div class="col-md-6">
<div class="admin-form-group">
<label class="admin-form-label">
<i class="fas fa-phone me-2"></i>Phone Number
</label>
<input type="text"
class="admin-form-control"
name="no_telp"
value="{{ $user->no_telp }}"
required>
</div>
</div>
<div class="col-md-6">
<div class="admin-form-group">
<label class="admin-form-label">
<i class="fas fa-user-tag me-2"></i>Role
</label>
<select class="admin-form-control" name="role_id" required>
@foreach ($roles as $id => $name)
<option value="{{ $id }}" {{ $user->role_id == $id ? 'selected' : '' }}>
{{ $name }}
</option>
@endforeach
</select>
</div>
</div>
<div class="col-md-6">
<div class="admin-form-group">
<label class="admin-form-label">
<i class="fas fa-key me-2"></i>Password
</label>
<div class="input-group">
<input type="password"
class="admin-form-control"
name="password"
id="passwordField"
placeholder="Leave blank to keep current password">
<button type="button"
class="btn btn-outline-secondary"
onclick="togglePasswordVisibility('passwordField', 'toggleIcon')">
<i class="fas fa-eye" id="toggleIcon"></i>
</button>
</div>
<small class="text-muted">Only fill this if you want to change the password</small>
</div>
</div>
<div class="col-md-6">
<div class="admin-form-group">
<label class="admin-form-label">
<i class="fas fa-key me-2"></i>Confirm Password
</label>
<div class="input-group">
<input type="password"
class="admin-form-control"
name="password_confirmation"
id="confirmPasswordField"
placeholder="Confirm new password">
<button type="button"
class="btn btn-outline-secondary"
onclick="togglePasswordVisibility('confirmPasswordField', 'toggleConfirmIcon')">
<i class="fas fa-eye" id="toggleConfirmIcon"></i>
</button>
</div>
</div>
</div>
</div>
<div class="text-end mt-4">
<button type="reset" class="btn btn-secondary me-2">
<i class="fas fa-undo me-2"></i>Reset
</button>
<a href="{{ route('datauser') }}" class="btn btn-secondary me-2">
<i class="fas fa-arrow-left me-2"></i>Back
</a>
<button type="button" class="btn btn-primary" onclick="confirmSave()">
<i class="fas fa-save me-2"></i>Save Changes
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<style>
.admin-form-group {
margin-bottom: 1.5rem;
}
.admin-form-label {
display: flex;
align-items: center;
font-weight: 500;
margin-bottom: 0.5rem;
color: #333;
}
.admin-form-label i {
width: 20px;
color: #2196f3;
}
.admin-form-control {
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 0.75rem 1rem;
width: 100%;
transition: all 0.3s ease;
}
.admin-form-control:focus {
border-color: #2196f3;
box-shadow: 0 0 0 0.2rem rgba(33, 150, 243, 0.25);
}
.input-group .btn {
padding: 0.75rem;
border: 1px solid #dee2e6;
background: #f8f9fa;
color: #6c757d;
}
.input-group .btn:hover {
background: #e9ecef;
color: #2196f3;
}
.input-group .admin-form-control {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.input-group .btn {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.text-muted {
font-size: 0.875rem;
margin-top: 0.25rem;
}
/* Responsive Adjustments */
@media (max-width: 768px) {
.admin-form-control {
padding: 0.625rem 0.875rem;
}
.input-group .btn {
padding: 0.625rem;
}
}
</style>
function confirmSave() {
Swal.fire({
title: 'Are you sure?',
text: "Do you want to save the changes?",
icon: 'question',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Yes, save it!'
}).then((result) => {
if (result.isConfirmed) {
document.getElementById('editUserForm').submit();
}
});
}
</script>
@endsection
@push('scripts')
<!-- SweetAlert2 -->
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script>
function togglePasswordVisibility(fieldId, iconId) {
var field = document.getElementById(fieldId);
var icon = document.getElementById(iconId);
if (field.type === "password") {
field.type = "text";
icon.classList.remove('fa-eye');
icon.classList.add('fa-eye-slash');
} else {
field.type = "password";
icon.classList.remove('fa-eye-slash');
icon.classList.add('fa-eye');
}
}
function confirmSave() {
// Get password fields
const password = document.getElementById('passwordField').value;
const confirmPassword = document.getElementById('confirmPasswordField').value;
// Check if either password field is filled
if (password || confirmPassword) {
// If one is filled, both must be filled and match
if (!password || !confirmPassword) {
Swal.fire({
title: 'Password Error',
text: 'Both password fields must be filled when changing password',
icon: 'error',
confirmButtonColor: '#2196f3',
confirmButtonText: 'OK'
});
return;
}
if (password !== confirmPassword) {
Swal.fire({
title: 'Password Mismatch',
text: 'Password and confirmation do not match',
icon: 'error',
confirmButtonColor: '#2196f3',
confirmButtonText: 'OK'
});
return;
}
}
Swal.fire({
title: 'Save Changes',
text: "Are you sure you want to update this user?",
icon: 'question',
showCancelButton: true,
confirmButtonColor: '#2196f3',
cancelButtonColor: '#6c757d',
confirmButtonText: '<i class="fas fa-save me-2"></i>Yes, save it!',
cancelButtonText: '<i class="fas fa-times me-2"></i>Cancel'
}).then((result) => {
if (result.isConfirmed) {
document.getElementById('editUserForm').submit();
}
});
}
document.addEventListener('DOMContentLoaded', function() {
const animateElements = document.querySelectorAll('.animate-fade-in');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.opacity = 1;
entry.target.style.transform = 'translateY(0)';
}
});
});
animateElements.forEach(element => {
element.style.opacity = 0;
element.style.transform = 'translateY(20px)';
observer.observe(element);
});
});
</script>
@endpush

View File

@ -1,93 +1,300 @@
@extends('layout.app')
@section('title', 'Add New User')
@include('admin.shared.admin-styles')
@section('content')
<div class="pagetitle">
<h1>Tambah Data User</h1>
<nav>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}">Home</a></li>
<li class="breadcrumb-item active"><a href="{{ route('datauser') }}">User</a></li>
<li class="breadcrumb-item active"><a href="{{ route('tambahuser') }}">Tambah User</a></li>
</ol>
</nav>
</div><!-- End Page Title -->
<div class="card">
<div class="card-body">
<h5 class="card-title">Tambah User</h5>
<!-- General Form Elements -->
<form id="AddUserForm" method="POST" action="{{ route('storeuser') }}">
@csrf <!-- Add this to include CSRF token -->
<div class="row mb-3">
<label for="inputText" class="col-sm-2 col-form-label">Name</label>
<div class="col-sm-10">
<input type="text" class="form-control" placeholder="Your name" aria-label="Your Name"
aria-describedby="basic-addon1" name="name" required>
</div>
</div>
<div class="row mb-3">
<label for="inputEmail" class="col-sm-2 col-form-label">Email</label>
<div class="col-sm-10">
<input type="email" class="form-control" name="email" required>
</div>
</div>
<div class="row mb-3">
<label for="inputNoTelp" class="col-sm-2 col-form-label">No. Telp</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="no_telp" required>
<div class="admin-container container-fluid">
<!-- Page Header -->
<div class="page-header animate-fade-in">
<div class="row align-items-center">
<div class="col-12">
<h3 class="mb-2 text-white">
<i class="fas fa-user-plus me-2"></i>Add New User
</h3>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}" class="text-white-50">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{{ route('datauser') }}" class="text-white-50">Users</a></li>
<li class="breadcrumb-item active text-white" aria-current="page">Add User</li>
</ol>
</nav>
</div>
</div>
<div class="row mb-3">
<label for="inputPassword" class="col-sm-2 col-form-label">Password</label>
<div class="col-sm-10">
<input type="password" class="form-control" placeholder="Input min 4 characters"
aria-label="Password" aria-describedby="basic-addon1" name="password" required>
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label">Role</label>
<div class="col-sm-10">
<select class="form-select" name="role_id" aria-label="Select Role" required>
<option selected disabled>Select a role</option>
@foreach($roles as $role)
<option value="{{ $role->id }}">{{ $role->name }}</option>
@endforeach
</select>
</div>
</div>
<div class="row mb-3">
<div class="col-sm-12 text-center">
<a href="{{ route('datauser') }}" class="btn btn-secondary mx-3">Back</a>
<button type="button" class="btn btn-primary mx-3" onclick="confirmSave()">Save</button>
</div>
</div>
</form><!-- End General Form Elements -->
</div>
</div>
<div class="row">
<div class="col-12">
<div class="admin-card animate-fade-in">
<div class="card-body">
<h5 class="card-title mb-4">
<i class="fas fa-user-edit me-2"></i>User Information
</h5>
<form id="AddUserForm" method="POST" action="{{ route('storeuser') }}">
@csrf
<div class="row g-4">
<div class="col-md-6">
<div class="admin-form-group">
<label class="admin-form-label">
<i class="fas fa-user me-2"></i>Name
</label>
<input type="text"
class="admin-form-control"
name="name"
placeholder="Enter user's name"
required>
</div>
</div>
<div class="col-md-6">
<div class="admin-form-group">
<label class="admin-form-label">
<i class="fas fa-envelope me-2"></i>Email
</label>
<input type="email"
class="admin-form-control"
name="email"
placeholder="Enter email address"
required>
</div>
</div>
<div class="col-md-6">
<div class="admin-form-group">
<label class="admin-form-label">
<i class="fas fa-phone me-2"></i>Phone Number
</label>
<input type="text"
class="admin-form-control"
name="no_telp"
placeholder="Enter phone number"
required>
</div>
</div>
<div class="col-md-6">
<div class="admin-form-group">
<label class="admin-form-label">
<i class="fas fa-user-tag me-2"></i>Role
</label>
<select class="admin-form-control" name="role_id" required>
<option value="" disabled selected>Select a role</option>
@foreach($roles as $role)
<option value="{{ $role->id }}">{{ $role->name }}</option>
@endforeach
</select>
</div>
</div>
<div class="col-md-6">
<div class="admin-form-group">
<label class="admin-form-label">
<i class="fas fa-key me-2"></i>Password
</label>
<div class="input-group">
<input type="password"
class="admin-form-control"
name="password"
id="passwordField"
placeholder="Minimum 4 characters"
required>
<button type="button"
class="btn btn-outline-secondary"
onclick="togglePasswordVisibility('passwordField', 'toggleIcon')">
<i class="fas fa-eye" id="toggleIcon"></i>
</button>
</div>
</div>
</div>
<div class="col-md-6">
<div class="admin-form-group">
<label class="admin-form-label">
<i class="fas fa-key me-2"></i>Confirm Password
</label>
<div class="input-group">
<input type="password"
class="admin-form-control"
name="password_confirmation"
id="confirmPasswordField"
placeholder="Re-enter password"
required>
<button type="button"
class="btn btn-outline-secondary"
onclick="togglePasswordVisibility('confirmPasswordField', 'toggleConfirmIcon')">
<i class="fas fa-eye" id="toggleConfirmIcon"></i>
</button>
</div>
</div>
</div>
</div>
<div class="text-end mt-4">
<a href="{{ route('datauser') }}" class="btn btn-secondary me-2">
<i class="fas fa-arrow-left me-2"></i>Back
</a>
<button type="button" class="btn btn-primary" onclick="confirmSave()">
<i class="fas fa-save me-2"></i>Save User
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<style>
.admin-form-group {
margin-bottom: 1.5rem;
}
.admin-form-label {
display: flex;
align-items: center;
font-weight: 500;
margin-bottom: 0.5rem;
color: #333;
}
.admin-form-label i {
width: 20px;
color: #2196f3;
}
.admin-form-control {
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 0.75rem 1rem;
width: 100%;
transition: all 0.3s ease;
}
.admin-form-control:focus {
border-color: #2196f3;
box-shadow: 0 0 0 0.2rem rgba(33, 150, 243, 0.25);
}
.input-group .btn {
padding: 0.75rem;
border: 1px solid #dee2e6;
background: #f8f9fa;
color: #6c757d;
}
.input-group .btn:hover {
background: #e9ecef;
color: #2196f3;
}
.input-group .admin-form-control {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.input-group .btn {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
/* Responsive Adjustments */
@media (max-width: 768px) {
.admin-form-control {
padding: 0.625rem 0.875rem;
}
.input-group .btn {
padding: 0.625rem;
}
}
</style>
@endsection
@push('scripts')
<!-- SweetAlert2 -->
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<!-- Confirmation Dialog Script -->
<script>
function confirmSave() {
function togglePasswordVisibility(fieldId, iconId) {
var field = document.getElementById(fieldId);
var icon = document.getElementById(iconId);
if (field.type === "password") {
field.type = "text";
icon.classList.remove('fa-eye');
icon.classList.add('fa-eye-slash');
} else {
field.type = "password";
icon.classList.remove('fa-eye-slash');
icon.classList.add('fa-eye');
}
}
function confirmSave() {
// Get password fields
const password = document.getElementById('passwordField').value;
const confirmPassword = document.getElementById('confirmPasswordField').value;
// Validate password
if (password.length < 4) {
Swal.fire({
title: 'Password Error',
text: 'Password must be at least 4 characters long',
icon: 'error',
confirmButtonColor: '#2196f3',
confirmButtonText: 'OK'
});
return;
}
// Check if passwords match
if (password !== confirmPassword) {
Swal.fire({
title: 'Password Mismatch',
text: 'Password and confirmation do not match',
icon: 'error',
confirmButtonColor: '#2196f3',
confirmButtonText: 'OK'
});
return;
}
Swal.fire({
title: 'Are you sure?',
text: "Do you want to save the changes?",
title: 'Save User',
text: "Are you sure you want to save this user?",
icon: 'question',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Yes, save it!'
confirmButtonColor: '#2196f3',
cancelButtonColor: '#6c757d',
confirmButtonText: '<i class="fas fa-save me-2"></i>Yes, save it!',
cancelButtonText: '<i class="fas fa-times me-2"></i>Cancel'
}).then((result) => {
if (result.isConfirmed) {
document.getElementById('AddUserForm').submit();
}
});
}
}
document.addEventListener('DOMContentLoaded', function() {
const animateElements = document.querySelectorAll('.animate-fade-in');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.opacity = 1;
entry.target.style.transform = 'translateY(0)';
}
});
});
animateElements.forEach(element => {
element.style.opacity = 0;
element.style.transform = 'translateY(20px)';
observer.observe(element);
});
});
</script>
@endsection
@endpush

View File

@ -0,0 +1,300 @@
@extends('layout.app')
@section('title', 'History Perhitungan AHP')
@section('content')
@include('admin.shared.admin-styles')
<div class="admin-container">
<!-- Page Header -->
<div class="page-header">
<div class="row align-items-center">
<div class="col-12">
<h3 class="mb-0">
<i class="fas fa-history me-2"></i>History Perhitungan AHP
</h3>
<p class="text-white-50 mb-3">Riwayat perhitungan AHP untuk rekomendasi menu makanan</p>
</div>
</div>
</div>
@if(session('error'))
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="fas fa-exclamation-circle me-2"></i>
{{ session('error') }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
@endif
<div class="row">
<div class="col-12">
<div class="admin-card">
<div class="card-header">
<div class="row align-items-center">
<div class="col-md-6 mb-3 mb-md-0">
<h5 class="card-title mb-0">
<i class="fas fa-calculator me-2"></i>Data Perhitungan
</h5>
</div>
<div class="col-md-6">
<form id="filterForm" class="d-flex gap-2">
<input type="date" class="form-control" id="tanggalFilter" name="tanggal" value="{{ $tanggal }}">
<button type="submit" class="admin-btn btn-primary">
<i class="fas fa-filter me-2"></i>Filter
</button>
</form>
</div>
</div>
</div>
<div class="card-body">
<!-- Nav tabs -->
<ul class="nav nav-pills nav-fill mb-4" role="tablist">
<li class="nav-item">
<a class="nav-link active" data-bs-toggle="tab" href="#bobotKriteria">
<i class="fas fa-weight me-2"></i>Bobot Kriteria
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#perbandinganKriteria">
<i class="fas fa-balance-scale me-2"></i>Perbandingan Kriteria
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#perbandinganAlternatif">
<i class="fas fa-random me-2"></i>Perbandingan Alternatif
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#consistencyRatio">
<i class="fas fa-check-circle me-2"></i>Consistency Ratio
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#skorMakanan">
<i class="fas fa-star me-2"></i>Bobot Alternatif atau Makanan
</a>
</li>
</ul>
<!-- Tab panes -->
<div class="tab-content">
<!-- Bobot Kriteria Tab -->
<div id="bobotKriteria" class="tab-pane active">
@forelse($bobotKriteria as $tanggalData => $bobots)
<div class="table-responsive">
<table class="admin-table">
<thead>
<tr>
<th style="width: 5%">No</th>
<th style="width: 65%">Kriteria</th>
<th style="width: 30%">Bobot</th>
</tr>
</thead>
<tbody>
@foreach($bobots as $index => $bobot)
<tr>
<td>{{ $index + 1 }}</td>
<td>{{ $bobot->kriteria->nama }}</td>
<td>{{ number_format($bobot->bobot, 4) }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@empty
<div class="text-center py-5">
<i class="fas fa-folder-open fa-3x text-muted mb-3"></i>
<p class="text-muted">Tidak ada data bobot kriteria untuk tanggal ini.</p>
</div>
@endforelse
</div>
<!-- Perbandingan Kriteria Tab -->
<div id="perbandinganKriteria" class="tab-pane fade">
@forelse($perbandinganKriteria as $tanggalData => $perbandinganKriterias)
<div class="table-responsive">
<table class="admin-table">
<thead>
<tr>
<th style="width: 5%">No</th>
<th style="width: 35%">Kriteria 1</th>
<th style="width: 35%">Kriteria 2</th>
<th style="width: 25%">Nilai</th>
</tr>
</thead>
<tbody>
@foreach($perbandinganKriterias as $index => $perbandingan)
<tr>
<td>{{ $index + 1 }}</td>
<td>{{ $perbandingan->kriteria1->nama }}</td>
<td>{{ $perbandingan->kriteria2->nama }}</td>
<td>{{ number_format($perbandingan->nilai, 4) }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@empty
<div class="text-center py-5">
<i class="fas fa-folder-open fa-3x text-muted mb-3"></i>
<p class="text-muted">Tidak ada data perbandingan kriteria untuk tanggal ini.</p>
</div>
@endforelse
</div>
<!-- Perbandingan Alternatif Tab -->
<div id="perbandinganAlternatif" class="tab-pane fade">
@forelse($perbandinganAlternatif as $tanggalData => $perbandinganAlternatifs)
<div class="table-responsive">
<table class="admin-table">
<thead>
<tr>
<th style="width: 5%">No</th>
<th style="width: 20%">Alternatif 1</th>
<th style="width: 20%">Alternatif 2</th>
<th style="width: 15%">Kriteria</th>
<th style="width: 20%">Waktu Makan</th>
<th style="width: 20%">Komponen</th>
<th style="width: 10%">Nilai</th>
</tr>
</thead>
<tbody>
@foreach($perbandinganAlternatifs as $index => $perbandingan)
<tr>
<td>{{ $index + 1 }}</td>
<td>{{ $perbandingan->alternatif1->nama }}</td>
<td>{{ $perbandingan->alternatif2->nama }}</td>
<td>{{ $perbandingan->kriteria->nama }}</td>
<td>{{ optional($perbandingan->waktuMakan)->nama ?? '-' }}</td>
<td>{{ optional($perbandingan->komponen)->nama ?? '-' }}</td>
<td>{{ number_format($perbandingan->nilai, 4) }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@empty
<div class="text-center py-5">
<i class="fas fa-folder-open fa-3x text-muted mb-3"></i>
<p class="text-muted">Tidak ada data perbandingan alternatif untuk tanggal ini.</p>
</div>
@endforelse
</div>
<!-- Consistency Ratio Tab -->
<div id="consistencyRatio" class="tab-pane fade">
@forelse($consistencyRatios as $tanggalData => $CRs)
<div class="table-responsive">
<table class="admin-table">
<thead>
<tr>
<th style="width: 5%">No</th>
<th style="width: 20%">Kriteria</th>
<th style="width: 20%">Waktu Makan</th>
<th style="width: 20%">Komponen</th>
<th style="width: 15%">Nilai CR</th>
<th style="width: 20%">Status</th>
</tr>
</thead>
<tbody>
@foreach($CRs as $index => $cr)
<tr>
<td>{{ $index + 1 }}</td>
<td>{{ optional($cr->kriteria)->nama ?? 'Semua Kriteria' }}</td>
<td>{{ optional($cr->waktuMakan)->nama ?? '-' }}</td>
<td>{{ optional($cr->komponen)->nama ?? '-' }}</td>
<td>{{ number_format($cr->nilai_cr, 4) }}</td>
<td>
<span class="admin-badge {{ $cr->nilai_cr < 0.1 ? 'bg-success' : 'bg-warning' }}">
{{ $cr->nilai_cr < 0.1 ? 'Konsisten' : 'Perlu Review' }}
</span>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@empty
<div class="text-center py-5">
<i class="fas fa-folder-open fa-3x text-muted mb-3"></i>
<p class="text-muted">Tidak ada data consistency ratio untuk tanggal ini.</p>
</div>
@endforelse
</div>
<!-- Skor Makanan Tab -->
<div id="skorMakanan" class="tab-pane fade">
@forelse($skorMakanan as $tanggalData => $skors)
<div class="table-responsive">
<table class="admin-table">
<thead>
<tr>
<th style="width: 5%">No</th>
<th style="width: 25%">Makanan</th>
<th style="width: 20%">Kriteria</th>
<th style="width: 20%">Waktu Makan</th>
<th style="width: 20%">Komponen</th>
<th style="width: 10%">Nilai</th>
</tr>
</thead>
<tbody>
@foreach($skors as $index => $skor)
<tr>
<td>{{ $index + 1 }}</td>
<td>{{ $skor->makanan->nama }}</td>
<td>{{ $skor->kriteria->nama }}</td>
<td>{{ optional($skor->waktuMakan)->nama ?? '-' }}</td>
<td>{{ optional($skor->komponen)->nama ?? '-' }}</td>
<td>{{ number_format($skor->nilai, 4) }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@empty
<div class="text-center py-5">
<i class="fas fa-folder-open fa-3x text-muted mb-3"></i>
<p class="text-muted">Tidak ada data skor makanan untuk tanggal ini.</p>
</div>
@endforelse
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
// Inisialisasi tooltips
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl)
});
// Filter form handling
const filterForm = document.getElementById('filterForm');
if (filterForm) {
filterForm.addEventListener('submit', function(e) {
e.preventDefault();
const tanggal = document.getElementById('tanggalFilter').value;
window.location.href = `{{ route('history.ahp') }}?tanggal=${tanggal}`;
});
}
// Simpan tab aktif ke localStorage
$('a[data-bs-toggle="tab"]').on('shown.bs.tab', function (e) {
localStorage.setItem('lastActiveHistoryTab', $(e.target).attr('href'));
});
// Restore tab aktif dari localStorage
var lastTab = localStorage.getItem('lastActiveHistoryTab');
if (lastTab) {
$('a[href="' + lastTab + '"]').tab('show');
}
});
</script>
@endpush

View File

@ -1,73 +1,159 @@
@extends('layout.app')
@section('title', 'Edit Jenis Makanan')
@include('admin.shared.admin-styles')
@section('content')
<div class="pagetitle">
<h1>Edit Data Jenis Makanan</h1>
<nav>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}">Home</a></li>
<li class="breadcrumb-item active"><a href="{{ route('jenismakanan') }}">Jenis Makanan</a></li>
<li class="breadcrumb-item active"><a href="{{ route('editjenismakanan', $jenis->id) }}">Edit Jenis Makanan</a></li>
</ol>
</nav>
</div><!-- End Page Title -->
<div class="card">
<div class="card-body">
<h5 class="card-title">Edit Jenis Makanan</h5>
<!-- General Form Elements -->
<form id="editUserForm" method="POST" action="{{ url("/jenismakanan/{$jenis->id}") }}">
@method('PUT')
@csrf
<div class="row mb-3">
<label for="inputText" class="col-sm-2 col-form-label">Name</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="name" value="{{ $jenis->name }}">
</div>
</div>
<div class="row mb-3">
<div class="col-sm-12 text-center">
<a href="{{ route('jenismakanan') }}" class="btn btn-secondary mx-3">Back</a>
<button type="button" class="btn btn-primary mx-3" onclick="confirmSave()">Save</button>
<!-- Reset button to reset form fields -->
<button type="reset" class="btn btn-secondary">Reset</button>
</div>
</div>
</form><!-- End General Form Elements -->
<div class="admin-container container-fluid">
<!-- Page Header -->
<div class="page-header animate-fade-in">
<div class="row align-items-center">
<div class="col-12">
<h3 class="mb-2 text-white">
<i class="fas fa-utensils me-2"></i>Edit Jenis Makanan
</h3>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}" class="text-white-50">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{{ route('jenismakanan') }}" class="text-white-50">Jenis Makanan</a></li>
<li class="breadcrumb-item active text-white" aria-current="page">Edit Jenis Makanan</li>
</ol>
</nav>
</div>
</div>
</div>
<!-- SweetAlert2 -->
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<div class="row">
<div class="col-12">
<div class="admin-card animate-fade-in">
<div class="card-body">
<h5 class="card-title mb-4">
<i class="fas fa-edit me-2"></i>Informasi Jenis Makanan
</h5>
<!-- Confirmation Dialog Script -->
<script>
function togglePasswordVisibility() {
var passwordField = document.getElementById("passwordField");
if (passwordField.type === "password") {
passwordField.type = "text";
document.querySelector('button[onclick="togglePasswordVisibility()"]').textContent = "Hide";
} else {
passwordField.type = "password";
document.querySelector('button[onclick="togglePasswordVisibility()"]').textContent = "Show";
}
}
<form id="editUserForm" method="POST" action="{{ url("/jenismakanan/{$jenis->id}") }}">
@method('PUT')
@csrf
<div class="row g-4">
<div class="col-md-12">
<div class="admin-form-group">
<label class="admin-form-label">
<i class="fas fa-tag me-2"></i>Nama Jenis Makanan
</label>
<input type="text"
class="admin-form-control @error('name') is-invalid @enderror"
name="name"
value="{{ $jenis->name }}"
placeholder="Masukkan nama jenis makanan"
required>
@error('name')
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
</div>
</div>
<div class="text-end mt-4">
<button type="reset" class="btn btn-secondary me-2">
<i class="fas fa-undo me-2"></i>Reset
</button>
<a href="{{ route('jenismakanan') }}" class="btn btn-secondary me-2">
<i class="fas fa-arrow-left me-2"></i>Kembali
</a>
<button type="button" class="btn btn-primary" onclick="confirmSave()">
<i class="fas fa-save me-2"></i>Simpan Perubahan
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<style>
.admin-form-group {
margin-bottom: 1.5rem;
}
.admin-form-label {
display: flex;
align-items: center;
font-weight: 500;
margin-bottom: 0.5rem;
color: #333;
}
.admin-form-label i {
width: 20px;
color: #2196f3;
}
.admin-form-control {
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 0.75rem 1rem;
width: 100%;
transition: all 0.3s ease;
}
.admin-form-control:focus {
border-color: #2196f3;
box-shadow: 0 0 0 0.2rem rgba(33, 150, 243, 0.25);
}
/* Responsive Adjustments */
@media (max-width: 768px) {
.admin-form-control {
padding: 0.625rem 0.875rem;
}
}
</style>
function confirmSave() {
Swal.fire({
title: 'Are you sure?',
text: "Do you want to save the changes?",
icon: 'question',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Yes, save it!'
}).then((result) => {
if (result.isConfirmed) {
document.getElementById('editUserForm').submit();
}
});
}
</script>
@endsection
@push('scripts')
<!-- SweetAlert2 -->
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script>
function confirmSave() {
Swal.fire({
title: 'Simpan Perubahan',
text: "Apakah Anda yakin ingin menyimpan perubahan ini?",
icon: 'question',
showCancelButton: true,
confirmButtonColor: '#2196f3',
cancelButtonColor: '#6c757d',
confirmButtonText: '<i class="fas fa-save me-2"></i>Ya, simpan!',
cancelButtonText: '<i class="fas fa-times me-2"></i>Batal'
}).then((result) => {
if (result.isConfirmed) {
document.getElementById('editUserForm').submit();
}
});
}
document.addEventListener('DOMContentLoaded', function() {
const animateElements = document.querySelectorAll('.animate-fade-in');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.opacity = 1;
entry.target.style.transform = 'translateY(0)';
}
});
});
animateElements.forEach(element => {
element.style.opacity = 0;
element.style.transform = 'translateY(20px)';
observer.observe(element);
});
});
</script>
@endpush

View File

@ -1,59 +1,148 @@
@extends('layout.app')
@section('title', 'Food Types Management')
@include('admin.shared.admin-styles')
@section('content')
<div class="pagetitle">
<div class="admin-container container-fluid">
<!-- Page Header -->
<div class="page-header animate-fade-in">
<div class="row align-items-center">
<div class="col-12">
<h3 class="mb-2 text-white">
<i class="fas fa-layer-group me-2"></i>Food Types Management
</h3>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}" class="text-white-50">Dashboard</a></li>
<li class="breadcrumb-item active text-white" aria-current="page">Food Types</li>
</ol>
</nav>
</div>
</div>
</div>
<h1>Jenis Makanan</h1>
<nav>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}">Home</a></li>
<li class="breadcrumb-item active"><a href="{{ route('jenismakanan') }}"> Jenis Makanan</a></li>
</ol>
</nav>
</div><!-- End Page Title -->
<section class="section">
<div class="row">
<div class="col-lg-12">
<div class="card">
<div class="col-12">
<div class="admin-card animate-fade-in">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<h5 class="card-title mb-2">Jenis Makanan</h5>
<div class="text-center">
<a href="{{ route('tambahjenismakanan') }}" class="btn btn-primary">Tambah</a>
</div>
<!-- Header with Add Button -->
<div class="d-flex justify-content-between align-items-center mb-4">
<h5 class="card-title mb-0">
<i class="fas fa-list me-2"></i>Food Types List
</h5>
<a href="{{ route('tambahjenismakanan') }}" class="admin-btn btn-primary">
<i class="fas fa-plus me-2"></i>Add New Food Type
</a>
</div>
<!-- Table with stripped rows -->
<table class="table datatable">
<thead>
<tr>
<th class="text-center">No</th>
<th class="text-center">Jenis Makanan</th>
<th class="text-center">Action</th>
</tr>
</thead>
<tbody>
@foreach ($jenis_makanans as $jm)
<tr>
<td class="text-center">{{ $loop->iteration }}</td>
<td class="text-center">{{ $jm->name }}</td>
<td class="text-center">
<a href="{{ route('editjenismakanan', $jm->id) }}" class="btn btn-sm btn-warning">Edit</a>
<form action="{{ url('/jenismakanan', $jm->id) }}" method="POST" style="display: inline;">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-sm btn-danger">Hapus</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
<!-- End Table with stripped rows -->
<!-- Food Types Table -->
<div class="table-responsive">
<table class="admin-table">
<thead>
<tr>
<th>No</th>
<th>Type Name</th>
<th class="text-center">Actions</th>
</tr>
</thead>
<tbody>
@foreach ($jenis_makanans as $jm)
<tr>
<td>{{ ($jenis_makanans->currentPage() - 1) * $jenis_makanans->perPage() + $loop->iteration }}</td>
<td>
<div class="d-flex align-items-center">
{{ $jm->name }}
</div>
</td>
<td class="text-center">
<a href="{{ route('editjenismakanan', $jm->id) }}"
class="admin-btn btn-warning btn-sm me-2">
<i class="fas fa-edit"></i>
</a>
<form action="{{ url('/jenismakanan', $jm->id) }}"
method="POST"
class="d-inline"
onsubmit="return confirm('Are you sure you want to delete this food type?');">
@csrf
@method('DELETE')
<button type="submit" class="admin-btn btn-danger btn-sm">
<i class="fas fa-trash"></i>
</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
<!-- Pagination -->
<div class="d-flex justify-content-between align-items-center mt-4">
<div class="text-muted">
Showing {{ $jenis_makanans->firstItem() ?? 0 }} to {{ $jenis_makanans->lastItem() ?? 0 }} of {{ $jenis_makanans->total() }} entries
</div>
<div class="admin-pagination">
{{ $jenis_makanans->links() }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
@endsection
</div>
@endsection
@push('styles')
<style>
.admin-pagination nav {
margin-bottom: 0;
}
.admin-pagination .pagination {
margin-bottom: 0;
}
.admin-pagination .page-link {
padding: 0.375rem 0.75rem;
font-size: 0.9rem;
border-radius: 0.25rem;
margin: 0 0.125rem;
color: var(--primary-color);
background-color: var(--white);
border: 1px solid var(--border-color);
}
.admin-pagination .page-item.active .page-link {
background-color: var(--primary-color);
border-color: var(--primary-color);
color: var(--white);
}
.admin-pagination .page-item.disabled .page-link {
color: var(--gray);
pointer-events: none;
background-color: var(--light);
border-color: var(--border-color);
}
</style>
@endpush
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
const animateElements = document.querySelectorAll('.animate-fade-in');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.opacity = 1;
entry.target.style.transform = 'translateY(0)';
}
});
});
animateElements.forEach(element => {
element.style.opacity = 0;
element.style.transform = 'translateY(20px)';
observer.observe(element);
});
});
</script>
@endpush

View File

@ -1,67 +1,158 @@
@extends('layout.app')
@section('title', 'Tambah Jenis Makanan')
@include('admin.shared.admin-styles')
@section('content')
<div class="pagetitle">
<div class="admin-container container-fluid">
<!-- Page Header -->
<div class="page-header animate-fade-in">
<div class="row align-items-center">
<div class="col-12">
<h3 class="mb-2 text-white">
<i class="fas fa-plus me-2"></i>Tambah Jenis Makanan
</h3>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}" class="text-white-50">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{{ route('jenismakanan') }}" class="text-white-50">Jenis Makanan</a></li>
<li class="breadcrumb-item active text-white" aria-current="page">Tambah Jenis Makanan</li>
</ol>
</nav>
</div>
</div>
</div>
<h1>Tambah Data Jenis Makanan</h1>
<nav>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}">Home</a></li>
<li class="breadcrumb-item active"><a href="{{ route('jenismakanan') }}">Jenis Makanan</a></li>
<li class="breadcrumb-item active"><a href="{{ route('tambahjenismakanan') }}">Tambah Jenis Makanan</a></li>
</ol>
</nav>
<div class="row">
<div class="col-12">
<div class="admin-card animate-fade-in">
<div class="card-body">
<h5 class="card-title mb-4">
<i class="fas fa-utensils me-2"></i>Informasi Jenis Makanan
</h5>
</div><!-- End Page Title -->
<form id="AddUserForm" method="POST" action="{{ url('/jenismakanan/storejenismakanan') }}">
@csrf
<div class="card">
<div class="card-body">
<h5 class="card-title">Tambah Jenis Makanan</h5>
<div class="row g-4">
<div class="col-md-12">
<div class="admin-form-group">
<label class="admin-form-label">
<i class="fas fa-tag me-2"></i>Nama Jenis Makanan
</label>
<input type="text"
class="admin-form-control @error('name') is-invalid @enderror"
name="name"
value="{{ old('name') }}"
placeholder="Masukkan nama jenis makanan"
required>
@error('name')
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
</div>
</div>
<!-- General Form Elements -->
<form id="AddUserForm" method="POST" action="{{ url('/jenismakanan/storejenismakanan') }}">
@csrf <!-- Add this to include CSRF token -->
<div class="row mb-3">
<label for="inputText" class="col-sm-2 col-form-label">Jenis Makanan</label>
<div class="col-sm-10">
<input type="text" class="form-control" placeholder="jenismakanan" aria-label="Your Name"
aria-describedby="basic-addon1" name="name">
</div>
</div>
<div class="row mb-3">
<div class="col-sm-12 text-center">
<a href="{{ route('jenismakanan') }}" class="btn btn-secondary mx-3">Back</a>
<button type="button" class="btn btn-primary mx-3" onclick="confirmSave()">Save</button>
</div>
</div>
</form><!-- End General Form Elements -->
</div>
<div class="text-end mt-4">
<button type="reset" class="btn btn-secondary me-2">
<i class="fas fa-undo me-2"></i>Reset
</button>
<a href="{{ route('jenismakanan') }}" class="btn btn-secondary me-2">
<i class="fas fa-arrow-left me-2"></i>Kembali
</a>
<button type="button" class="btn btn-primary" onclick="confirmSave()">
<i class="fas fa-save me-2"></i>Simpan
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
.admin-form-group {
margin-bottom: 1.5rem;
}
.admin-form-label {
display: flex;
align-items: center;
font-weight: 500;
margin-bottom: 0.5rem;
color: #333;
}
.admin-form-label i {
width: 20px;
color: #2196f3;
}
.admin-form-control {
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 0.75rem 1rem;
width: 100%;
transition: all 0.3s ease;
}
.admin-form-control:focus {
border-color: #2196f3;
box-shadow: 0 0 0 0.2rem rgba(33, 150, 243, 0.25);
}
/* Responsive Adjustments */
@media (max-width: 768px) {
.admin-form-control {
padding: 0.625rem 0.875rem;
}
}
</style>
@endsection
@push('scripts')
<!-- SweetAlert2 -->
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<!-- Confirmation Dialog Script -->
<script>
function confirmSave() {
function confirmSave() {
Swal.fire({
title: 'Are you sure?',
text: "Do you want to save the changes?",
title: 'Simpan Data',
text: "Apakah Anda yakin ingin menyimpan data ini?",
icon: 'question',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Yes, save it!'
confirmButtonColor: '#2196f3',
cancelButtonColor: '#6c757d',
confirmButtonText: '<i class="fas fa-save me-2"></i>Ya, simpan!',
cancelButtonText: '<i class="fas fa-times me-2"></i>Batal'
}).then((result) => {
if (result.isConfirmed) {
console.log("Form submitted"); // Tambahkan ini di dalam `if (result.isConfirmed)` sebelum submit form
document.getElementById('AddUserForm').submit();
}
});
}
}
document.addEventListener('DOMContentLoaded', function() {
const animateElements = document.querySelectorAll('.animate-fade-in');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.opacity = 1;
entry.target.style.transform = 'translateY(0)';
}
});
});
animateElements.forEach(element => {
element.style.opacity = 0;
element.style.transform = 'translateY(20px)';
observer.observe(element);
});
});
</script>
@endsection
@endpush

View File

@ -1,76 +1,180 @@
@extends('layout.app')
@section('title', 'Edit Kategori')
@include('admin.shared.admin-styles')
@section('content')
<div class="pagetitle">
<h1>Edit Data Kategori</h1>
<nav>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}">Home</a></li>
<li class="breadcrumb-item active"><a href="{{ route('kategori') }}">Kategori</a></li>
<li class="breadcrumb-item active"><a href="{{ route('editkategori', $kategori->id) }}">Edit Kategori</a></li>
</ol>
</nav>
</div><!-- End Page Title -->
<div class="card">
<div class="card-body">
<h5 class="card-title">Edit Kategori</h5>
<!-- General Form Elements -->
<form id="editUserForm" method="POST" action="{{ url("/kategori/{$kategori->id}") }}">
@method('PUT')
@csrf
<div class="row mb-3">
<label for="inputText" class="col-sm-2 col-form-label">Name</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="kategori" value="{{ $kategori->kategori }}">
</div>
</div>
<div class="row mb-3">
<div class="col-sm-12 text-center">
<a href="{{ route('kategori') }}" class="btn btn-secondary mx-3">Back</a>
<button type="button" class="btn btn-primary mx-3" onclick="confirmSave()">Save</button>
<!-- Reset button to reset form fields -->
<button type="reset" class="btn btn-secondary">Reset</button>
</div>
</div>
</form><!-- End General Form Elements -->
<div class="admin-container container-fluid">
<!-- Page Header -->
<div class="page-header animate-fade-in">
<div class="row align-items-center">
<div class="col-12">
<h3 class="mb-2 text-white">
<i class="fas fa-edit me-2"></i>Edit Kategori
</h3>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}" class="text-white-50">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{{ route('kategori') }}" class="text-white-50">Kategori</a></li>
<li class="breadcrumb-item active text-white" aria-current="page">Edit Kategori</li>
</ol>
</nav>
</div>
</div>
</div>
<!-- SweetAlert2 -->
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<div class="row">
<div class="col-12">
<div class="admin-card animate-fade-in">
<div class="card-body">
<h5 class="card-title mb-4">
<i class="fas fa-tag me-2"></i>Detail Kategori
</h5>
<!-- Confirmation Dialog Script -->
<script>
function togglePasswordVisibility() {
var passwordField = document.getElementById("passwordField");
if (passwordField.type === "password") {
passwordField.type = "text";
document.querySelector('button[onclick="togglePasswordVisibility()"]').textContent = "Hide";
} else {
passwordField.type = "password";
document.querySelector('button[onclick="togglePasswordVisibility()"]').textContent = "Show";
}
}
<form id="editUserForm" method="POST" action="{{ url("/kategori/{$kategori->id}") }}">
@method('PUT')
@csrf
<div class="row g-4">
<div class="col-md-12">
<div class="admin-form-group">
<label class="admin-form-label">
<i class="fas fa-tag me-2"></i>Nama Kategori
</label>
<input type="text"
class="admin-form-control @error('kategori') is-invalid @enderror"
name="kategori"
value="{{ old('kategori', $kategori->kategori) }}"
placeholder="Masukkan nama kategori"
required>
@error('kategori')
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
</div>
</div>
<div class="text-end mt-4">
<button type="reset" class="btn btn-secondary me-2">
<i class="fas fa-undo me-2"></i>Reset
</button>
<a href="{{ route('kategori') }}" class="btn btn-secondary me-2">
<i class="fas fa-arrow-left me-2"></i>Kembali
</a>
<button type="button" class="btn btn-primary" onclick="confirmSave()">
<i class="fas fa-save me-2"></i>Simpan Perubahan
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<style>
.admin-form-group {
margin-bottom: 1.5rem;
}
.admin-form-label {
display: flex;
align-items: center;
font-weight: 500;
margin-bottom: 0.5rem;
color: #333;
}
.admin-form-label i {
width: 20px;
color: #2196f3;
}
.admin-form-control {
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 0.75rem 1rem;
width: 100%;
transition: all 0.3s ease;
}
.admin-form-control:focus {
border-color: #2196f3;
box-shadow: 0 0 0 0.2rem rgba(33, 150, 243, 0.25);
}
.admin-form-control.is-invalid {
border-color: #dc3545;
padding-right: calc(1.5em + 0.75rem);
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right calc(0.375em + 0.1875rem) center;
background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
}
.invalid-feedback {
display: none;
width: 100%;
margin-top: 0.25rem;
font-size: 0.875em;
color: #dc3545;
}
.is-invalid ~ .invalid-feedback {
display: block;
}
/* Responsive Adjustments */
@media (max-width: 768px) {
.admin-form-control {
padding: 0.625rem 0.875rem;
}
}
</style>
function confirmSave() {
Swal.fire({
title: 'Are you sure?',
text: "Do you want to save the changes?",
icon: 'question',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Yes, save it!'
}).then((result) => {
if (result.isConfirmed) {
document.getElementById('editUserForm').submit();
}
});
}
</script>
@endsection
@push('scripts')
<!-- SweetAlert2 -->
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script>
function confirmSave() {
Swal.fire({
title: 'Simpan Perubahan',
text: "Apakah Anda yakin ingin menyimpan perubahan ini?",
icon: 'question',
showCancelButton: true,
confirmButtonColor: '#2196f3',
cancelButtonColor: '#6c757d',
confirmButtonText: '<i class="fas fa-save me-2"></i>Ya, simpan!',
cancelButtonText: '<i class="fas fa-times me-2"></i>Batal'
}).then((result) => {
if (result.isConfirmed) {
document.getElementById('editUserForm').submit();
}
});
}
document.addEventListener('DOMContentLoaded', function() {
const animateElements = document.querySelectorAll('.animate-fade-in');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.opacity = 1;
entry.target.style.transform = 'translateY(0)';
}
});
});
animateElements.forEach(element => {
element.style.opacity = 0;
element.style.transform = 'translateY(20px)';
observer.observe(element);
});
});
</script>
@endpush

View File

@ -1,59 +1,148 @@
@extends('layout.app')
@section('title', 'Category Management')
@include('admin.shared.admin-styles')
@section('content')
<div class="pagetitle">
<div class="admin-container container-fluid">
<!-- Page Header -->
<div class="page-header animate-fade-in">
<div class="row align-items-center">
<div class="col-12">
<h3 class="mb-2 text-white">
<i class="fas fa-tags me-2"></i>Category Management
</h3>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}" class="text-white-50">Dashboard</a></li>
<li class="breadcrumb-item active text-white" aria-current="page">Categories</li>
</ol>
</nav>
</div>
</div>
</div>
<h1>Kategori</h1>
<nav>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}">Home</a></li>
<li class="breadcrumb-item active"><a href="{{ route('kategori') }}"> kategori</a></li>
</ol>
</nav>
</div><!-- End Page Title -->
<section class="section">
<div class="row">
<div class="col-lg-12">
<div class="card">
<div class="col-12">
<div class="admin-card animate-fade-in">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<h5 class="card-title mb-2">Kategori</h5>
<div class="text-center">
<a href="{{ route('tambahkategori') }}" class="btn btn-primary">Tambah</a>
</div>
<!-- Header with Add Button -->
<div class="d-flex justify-content-between align-items-center mb-4">
<h5 class="card-title mb-0">
<i class="fas fa-list me-2"></i>Category List
</h5>
<a href="{{ route('tambahkategori') }}" class="admin-btn btn-primary">
<i class="fas fa-plus me-2"></i>Add New Category
</a>
</div>
<!-- Table with stripped rows -->
<table class="table datatable">
<thead>
<tr>
<th class="text-center">No</th>
<th class="text-center">Kategori</th>
<th class="text-center">Action</th>
</tr>
</thead>
<tbody>
@foreach ($kategoris as $ktgr)
<tr>
<td class="text-center">{{ $loop->iteration }}</td>
<td class="text-center">{{ $ktgr->kategori }}</td>
<td class="text-center">
<a href="{{ route('editkategori', $ktgr->id) }}" class="btn btn-sm btn-warning">Edit</a>
<form action="{{ url('/kategori', $ktgr->id) }}" method="POST" style="display: inline;">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-sm btn-danger">Hapus</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
<!-- End Table with stripped rows -->
<!-- Category Table -->
<div class="table-responsive">
<table class="admin-table">
<thead>
<tr>
<th>No</th>
<th>Category Name</th>
<th class="text-center">Actions</th>
</tr>
</thead>
<tbody>
@foreach ($kategoris as $ktgr)
<tr>
<td>{{ ($kategoris->currentPage() - 1) * $kategoris->perPage() + $loop->iteration }}</td>
<td>
<div class="d-flex align-items-center">
{{ $ktgr->kategori }}
</div>
</td>
<td class="text-center">
<a href="{{ route('editkategori', $ktgr->id) }}"
class="admin-btn btn-warning btn-sm me-2">
<i class="fas fa-edit"></i>
</a>
<form action="{{ url('/kategori', $ktgr->id) }}"
method="POST"
class="d-inline"
onsubmit="return confirm('Are you sure you want to delete this category?');">
@csrf
@method('DELETE')
<button type="submit" class="admin-btn btn-danger btn-sm">
<i class="fas fa-trash"></i>
</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
<!-- Pagination -->
<div class="d-flex justify-content-between align-items-center mt-4">
<div class="text-muted">
Showing {{ $kategoris->firstItem() ?? 0 }} to {{ $kategoris->lastItem() ?? 0 }} of {{ $kategoris->total() }} entries
</div>
<div class="admin-pagination">
{{ $kategoris->links() }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
@endsection
</div>
@endsection
@push('styles')
<style>
.admin-pagination nav {
margin-bottom: 0;
}
.admin-pagination .pagination {
margin-bottom: 0;
}
.admin-pagination .page-link {
padding: 0.375rem 0.75rem;
font-size: 0.9rem;
border-radius: 0.25rem;
margin: 0 0.125rem;
color: var(--primary-color);
background-color: var(--white);
border: 1px solid var(--border-color);
}
.admin-pagination .page-item.active .page-link {
background-color: var(--primary-color);
border-color: var(--primary-color);
color: var(--white);
}
.admin-pagination .page-item.disabled .page-link {
color: var(--gray);
pointer-events: none;
background-color: var(--light);
border-color: var(--border-color);
}
</style>
@endpush
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
const animateElements = document.querySelectorAll('.animate-fade-in');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.opacity = 1;
entry.target.style.transform = 'translateY(0)';
}
});
});
animateElements.forEach(element => {
element.style.opacity = 0;
element.style.transform = 'translateY(20px)';
observer.observe(element);
});
});
</script>
@endpush

View File

@ -1,67 +1,179 @@
@extends('layout.app')
@section('title', 'Tambah Kategori')
@include('admin.shared.admin-styles')
@section('content')
<div class="pagetitle">
<div class="admin-container container-fluid">
<!-- Page Header -->
<div class="page-header animate-fade-in">
<div class="row align-items-center">
<div class="col-12">
<h3 class="mb-2 text-white">
<i class="fas fa-plus me-2"></i>Tambah Kategori
</h3>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}" class="text-white-50">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{{ route('kategori') }}" class="text-white-50">Kategori</a></li>
<li class="breadcrumb-item active text-white" aria-current="page">Tambah Kategori</li>
</ol>
</nav>
</div>
</div>
</div>
<h1>Tambah Data Kategori</h1>
<nav>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}">Home</a></li>
<li class="breadcrumb-item active"><a href="{{ route('kategori') }}">Kategori</a></li>
<li class="breadcrumb-item active"><a href="{{ route('tambahkategori') }}">Tambah Kategori</a></li>
</ol>
</nav>
<div class="row">
<div class="col-12">
<div class="admin-card animate-fade-in">
<div class="card-body">
<h5 class="card-title mb-4">
<i class="fas fa-tag me-2"></i>Detail Kategori
</h5>
</div><!-- End Page Title -->
<form id="AddUserForm" method="POST" action="{{ url('/kategori/storekategori') }}">
@csrf
<div class="card">
<div class="card-body">
<h5 class="card-title">Tambah Kategori</h5>
<div class="row g-4">
<div class="col-md-12">
<div class="admin-form-group">
<label class="admin-form-label">
<i class="fas fa-tag me-2"></i>Nama Kategori
</label>
<input type="text"
class="admin-form-control @error('kategori') is-invalid @enderror"
name="kategori"
value="{{ old('kategori') }}"
placeholder="Masukkan nama kategori"
required>
@error('kategori')
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
</div>
</div>
<!-- General Form Elements -->
<form id="AddUserForm" method="POST" action="{{ url('/kategori/storekategori') }}">
@csrf <!-- Add this to include CSRF token -->
<div class="row mb-3">
<label for="inputText" class="col-sm-2 col-form-label">Kategori</label>
<div class="col-sm-10">
<input type="text" class="form-control" placeholder="kategori" aria-label="Your Name"
aria-describedby="basic-addon1" name="kategori">
</div>
</div>
<div class="row mb-3">
<div class="col-sm-12 text-center">
<a href="{{ route('kategori') }}" class="btn btn-secondary mx-3">Back</a>
<button type="button" class="btn btn-primary mx-3" onclick="confirmSave()">Save</button>
</div>
</div>
</form><!-- End General Form Elements -->
</div>
<div class="text-end mt-4">
<button type="reset" class="btn btn-secondary me-2">
<i class="fas fa-undo me-2"></i>Reset
</button>
<a href="{{ route('kategori') }}" class="btn btn-secondary me-2">
<i class="fas fa-arrow-left me-2"></i>Kembali
</a>
<button type="button" class="btn btn-primary" onclick="confirmSave()">
<i class="fas fa-save me-2"></i>Simpan
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
.admin-form-group {
margin-bottom: 1.5rem;
}
.admin-form-label {
display: flex;
align-items: center;
font-weight: 500;
margin-bottom: 0.5rem;
color: #333;
}
.admin-form-label i {
width: 20px;
color: #2196f3;
}
.admin-form-control {
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 0.75rem 1rem;
width: 100%;
transition: all 0.3s ease;
}
.admin-form-control:focus {
border-color: #2196f3;
box-shadow: 0 0 0 0.2rem rgba(33, 150, 243, 0.25);
}
.admin-form-control.is-invalid {
border-color: #dc3545;
padding-right: calc(1.5em + 0.75rem);
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right calc(0.375em + 0.1875rem) center;
background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
}
.invalid-feedback {
display: none;
width: 100%;
margin-top: 0.25rem;
font-size: 0.875em;
color: #dc3545;
}
.is-invalid ~ .invalid-feedback {
display: block;
}
/* Responsive Adjustments */
@media (max-width: 768px) {
.admin-form-control {
padding: 0.625rem 0.875rem;
}
}
</style>
@endsection
@push('scripts')
<!-- SweetAlert2 -->
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<!-- Confirmation Dialog Script -->
<script>
function confirmSave() {
function confirmSave() {
Swal.fire({
title: 'Are you sure?',
text: "Do you want to save the changes?",
title: 'Simpan Data',
text: "Apakah Anda yakin ingin menyimpan data ini?",
icon: 'question',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Yes, save it!'
confirmButtonColor: '#2196f3',
cancelButtonColor: '#6c757d',
confirmButtonText: '<i class="fas fa-save me-2"></i>Ya, simpan!',
cancelButtonText: '<i class="fas fa-times me-2"></i>Batal'
}).then((result) => {
if (result.isConfirmed) {
console.log("Form submitted"); // Tambahkan ini di dalam `if (result.isConfirmed)` sebelum submit form
document.getElementById('AddUserForm').submit();
}
});
}
}
document.addEventListener('DOMContentLoaded', function() {
const animateElements = document.querySelectorAll('.animate-fade-in');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.opacity = 1;
entry.target.style.transform = 'translateY(0)';
}
});
});
animateElements.forEach(element => {
element.style.opacity = 0;
element.style.transform = 'translateY(20px)';
observer.observe(element);
});
});
</script>
@endsection
@endpush

View File

@ -1,57 +1,141 @@
@extends('layout.app')
@section('content')
<div class="pagetitle">
<h1>Data komponen</h1>
<nav>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}">Home</a></li>
<li class="breadcrumb-item active"><a href="{{ route('komponen') }}"> komponen</a></li>
</ol>
</nav>
</div><!-- End Page Title -->
<section class="section">
@section('title', 'Component Management')
@include('admin.shared.admin-styles')
@section('content')
<div class="admin-container container-fluid">
<!-- Page Header -->
<div class="page-header animate-fade-in">
<div class="row align-items-center">
<div class="col-12">
<h3 class="mb-2 text-white">
<i class="fas fa-puzzle-piece me-2"></i>Component Management
</h3>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}" class="text-white-50">Dashboard</a></li>
<li class="breadcrumb-item active text-white" aria-current="page">Components</li>
</ol>
</nav>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-12">
<div class="card">
<div class="col-12">
<div class="admin-card animate-fade-in">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<h5 class="card-title mb-2">Data komponen</h5>
{{-- <div class="text-center">
<a href="{{ route('tambahkomponen') }}" class="btn btn-primary">Tambah</a>
</div> --}}
<!-- Header -->
<div class="d-flex justify-content-between align-items-center mb-4">
<h5 class="card-title mb-0">
<i class="fas fa-list me-2"></i>Component List
</h5>
</div>
<!-- Table with stripped rows -->
<table class="table datatable">
<thead>
<tr>
<th class="text-center">No</th>
<th class="text-center">Name</th>
<th class="text-center">Action</th>
</tr>
</thead>
<tbody>
@foreach ($komponens as $kmpn)
<tr>
<td class="text-center">{{ $loop->iteration }}</td>
<td class="text-center">{{ $kmpn->nama }}</td>
<td class="text-center">
{{-- <a href="{{ route('edituser', $usr->id) }}" class="btn btn-sm btn-warning">Edit</a>
<form action="{{ route('deleteuser', $usr->id) }}" method="POST" class="d-inline delete-form">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-sm btn-danger delete-button">Hapus</button>
</form> --}}
</td>
</tr>
@endforeach
</tbody>
</table>
<!-- End Table with stripped rows -->
<!-- Component Table -->
<div class="table-responsive">
<table class="admin-table">
<thead>
<tr>
<th>No</th>
<th>Name</th>
<th class="text-center">Actions</th>
</tr>
</thead>
<tbody>
@foreach ($komponens as $kmpn)
<tr>
<td>{{ ($komponens->currentPage() - 1) * $komponens->perPage() + $loop->iteration }}</td>
<td>
<div class="d-flex align-items-center">
{{ $kmpn->nama }}
</div>
</td>
<td class="text-center">
<a href="#" class="admin-btn btn-info btn-sm" title="View Details">
<i class="fas fa-eye"></i>
</a>
<form action="{{ route('deletekomponen', $kmpn->id) }}" method="POST" class="d-inline" onsubmit="return confirm('Are you sure you want to delete this component?');">
@csrf
@method('DELETE')
<button type="submit" class="admin-btn btn-danger btn-sm" title="Delete">
<i class="fas fa-trash"></i>
</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
<!-- Pagination -->
<div class="d-flex justify-content-between align-items-center mt-4">
<div class="text-muted">
Showing {{ $komponens->firstItem() ?? 0 }} to {{ $komponens->lastItem() ?? 0 }} of {{ $komponens->total() }} entries
</div>
<div class="admin-pagination">
{{ $komponens->links() }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
@endsection
@push('styles')
<style>
.admin-pagination nav {
margin-bottom: 0;
}
.admin-pagination .pagination {
margin-bottom: 0;
}
.admin-pagination .page-link {
padding: 0.375rem 0.75rem;
font-size: 0.9rem;
border-radius: 0.25rem;
margin: 0 0.125rem;
color: var(--primary-color);
background-color: var(--white);
border: 1px solid var(--border-color);
}
.admin-pagination .page-item.active .page-link {
background-color: var(--primary-color);
border-color: var(--primary-color);
color: var(--white);
}
.admin-pagination .page-item.disabled .page-link {
color: var(--gray);
pointer-events: none;
background-color: var(--light);
border-color: var(--border-color);
}
</style>
@endpush
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
const animateElements = document.querySelectorAll('.animate-fade-in');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.opacity = 1;
entry.target.style.transform = 'translateY(0)';
}
});
});
animateElements.forEach(element => {
element.style.opacity = 0;
element.style.transform = 'translateY(20px)';
observer.observe(element);
});
});
</script>
@endpush

View File

@ -1,81 +1,199 @@
@extends('layout.app')
@section('title', 'Edit Kriteria')
@include('admin.shared.admin-styles')
@section('content')
<div class="pagetitle">
<h1>Edit Data User</h1>
<nav>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}">Home</a></li>
<li class="breadcrumb-item active"><a href="{{ route('kriteria') }}">User</a></li>
<li class="breadcrumb-item active"><a href="{{ route('editkriteria', $kriteria->id) }}">Edit User</a></li>
</ol>
</nav>
</div><!-- End Page Title -->
<div class="card">
<div class="card-body">
<h5 class="card-title">Edit User</h5>
<!-- General Form Elements -->
<form id="editUserForm" method="POST" action="{{ url("/kriteria/{$kriteria->id}") }}">
@method('PUT')
@csrf
<div class="row mb-3">
<label for="inputText" class="col-sm-2 col-form-label">Name</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="nama" value="{{ $kriteria->nama }}">
</div>
</div>
<div class="row mb-3">
<label for="inputText" class="col-sm-2 col-form-label">Satuan</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="satuan" value="{{ $kriteria->satuan }}">
</div>
</div>
<div class="row mb-3">
<div class="col-sm-12 text-center">
<a href="{{ route('kriteria') }}" class="btn btn-secondary mx-3">Back</a>
<button type="button" class="btn btn-primary mx-3" onclick="confirmSave()">Save</button>
<!-- Reset button to reset form fields -->
<button type="reset" class="btn btn-secondary">Reset</button>
</div>
</div>
</form><!-- End General Form Elements -->
<div class="admin-container container-fluid">
<!-- Page Header -->
<div class="page-header animate-fade-in">
<div class="row align-items-center">
<div class="col-12">
<h3 class="mb-2 text-white">
<i class="fas fa-edit me-2"></i>Edit Kriteria
</h3>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}" class="text-white-50">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{{ route('kriteria') }}" class="text-white-50">Kriteria</a></li>
<li class="breadcrumb-item active text-white" aria-current="page">Edit Kriteria</li>
</ol>
</nav>
</div>
</div>
</div>
<!-- SweetAlert2 -->
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<div class="row">
<div class="col-12">
<div class="admin-card animate-fade-in">
<div class="card-body">
<h5 class="card-title mb-4">
<i class="fas fa-list-check me-2"></i>Detail Kriteria
</h5>
<!-- Confirmation Dialog Script -->
<script>
function togglePasswordVisibility() {
var passwordField = document.getElementById("passwordField");
if (passwordField.type === "password") {
passwordField.type = "text";
document.querySelector('button[onclick="togglePasswordVisibility()"]').textContent = "Hide";
} else {
passwordField.type = "password";
document.querySelector('button[onclick="togglePasswordVisibility()"]').textContent = "Show";
}
}
<form id="editUserForm" method="POST" action="{{ url("/kriteria/{$kriteria->id}") }}">
@method('PUT')
@csrf
<div class="row g-4">
<div class="col-md-6">
<div class="admin-form-group">
<label class="admin-form-label">
<i class="fas fa-list-check me-2"></i>Nama Kriteria
</label>
<input type="text"
class="admin-form-control @error('nama') is-invalid @enderror"
name="nama"
value="{{ old('nama', $kriteria->nama) }}"
placeholder="Masukkan nama kriteria"
required>
@error('nama')
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
</div>
<div class="col-md-6">
<div class="admin-form-group">
<label class="admin-form-label">
<i class="fas fa-ruler me-2"></i>Satuan Ukur
</label>
<input type="text"
class="admin-form-control @error('satuan') is-invalid @enderror"
name="satuan"
value="{{ old('satuan', $kriteria->satuan) }}"
placeholder="Masukkan satuan ukur"
required>
@error('satuan')
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
</div>
</div>
<div class="text-end mt-4">
<button type="reset" class="btn btn-secondary me-2">
<i class="fas fa-undo me-2"></i>Reset
</button>
<a href="{{ route('kriteria') }}" class="btn btn-secondary me-2">
<i class="fas fa-arrow-left me-2"></i>Kembali
</a>
<button type="button" class="btn btn-primary" onclick="confirmSave()">
<i class="fas fa-save me-2"></i>Simpan Perubahan
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<style>
.admin-form-group {
margin-bottom: 1.5rem;
}
.admin-form-label {
display: flex;
align-items: center;
font-weight: 500;
margin-bottom: 0.5rem;
color: #333;
}
.admin-form-label i {
width: 20px;
color: #2196f3;
}
.admin-form-control {
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 0.75rem 1rem;
width: 100%;
transition: all 0.3s ease;
}
.admin-form-control:focus {
border-color: #2196f3;
box-shadow: 0 0 0 0.2rem rgba(33, 150, 243, 0.25);
}
.admin-form-control.is-invalid {
border-color: #dc3545;
padding-right: calc(1.5em + 0.75rem);
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right calc(0.375em + 0.1875rem) center;
background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
}
.invalid-feedback {
display: none;
width: 100%;
margin-top: 0.25rem;
font-size: 0.875em;
color: #dc3545;
}
.is-invalid ~ .invalid-feedback {
display: block;
}
/* Responsive Adjustments */
@media (max-width: 768px) {
.admin-form-control {
padding: 0.625rem 0.875rem;
}
}
</style>
function confirmSave() {
Swal.fire({
title: 'Are you sure?',
text: "Do you want to save the changes?",
icon: 'question',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Yes, save it!'
}).then((result) => {
if (result.isConfirmed) {
document.getElementById('editUserForm').submit();
}
});
}
</script>
@endsection
@push('scripts')
<!-- SweetAlert2 -->
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script>
function confirmSave() {
Swal.fire({
title: 'Simpan Perubahan',
text: "Apakah Anda yakin ingin menyimpan perubahan ini?",
icon: 'question',
showCancelButton: true,
confirmButtonColor: '#2196f3',
cancelButtonColor: '#6c757d',
confirmButtonText: '<i class="fas fa-save me-2"></i>Ya, simpan!',
cancelButtonText: '<i class="fas fa-times me-2"></i>Batal'
}).then((result) => {
if (result.isConfirmed) {
document.getElementById('editUserForm').submit();
}
});
}
document.addEventListener('DOMContentLoaded', function() {
const animateElements = document.querySelectorAll('.animate-fade-in');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.opacity = 1;
entry.target.style.transform = 'translateY(0)';
}
});
});
animateElements.forEach(element => {
element.style.opacity = 0;
element.style.transform = 'translateY(20px)';
observer.observe(element);
});
});
</script>
@endpush

View File

@ -1,61 +1,157 @@
@extends('layout.app')
@section('title', 'Criteria Management')
@include('admin.shared.admin-styles')
@section('content')
<div class="pagetitle">
<div class="admin-container container-fluid">
<!-- Page Header -->
<div class="page-header animate-fade-in">
<div class="row align-items-center">
<div class="col-12">
<h3 class="mb-2 text-white">
<i class="fas fa-list-check me-2"></i>Criteria Management
</h3>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}" class="text-white-50">Dashboard</a></li>
<li class="breadcrumb-item active text-white" aria-current="page">Criteria</li>
</ol>
</nav>
</div>
</div>
</div>
<h1>Kriteria</h1>
<nav>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}">Home</a></li>
<li class="breadcrumb-item active"><a href="{{ route('kriteria') }}"> Kriteria</a></li>
</ol>
</nav>
</div><!-- End Page Title -->
<section class="section">
<div class="row">
<div class="col-lg-12">
<div class="card">
<div class="col-12">
<div class="admin-card animate-fade-in">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<h5 class="card-title mb-2">Kriteria</h5>
<div class="text-center">
<a href="{{ route('tambahkriteria') }}" class="btn btn-primary">Tambah</a>
</div>
<!-- Header with Add Button -->
<div class="d-flex justify-content-between align-items-center mb-4">
<h5 class="card-title mb-0">
<i class="fas fa-list me-2"></i>Criteria List
</h5>
<a href="{{ route('tambahkriteria') }}" class="admin-btn btn-primary">
<i class="fas fa-plus me-2"></i>Add New Criteria
</a>
</div>
<!-- Table with stripped rows -->
<table class="table datatable">
<thead>
<tr>
<th class="text-center">No</th>
<th class="text-center">Nama</th>
<th class="text-center">Satuan</th>
<th class="text-center">Action</th>
</tr>
</thead>
<tbody>
@foreach ($kriterias as $krt)
<tr>
<td class="text-center">{{ $loop->iteration }}</td>
<td class="text-center">{{ $krt->nama }}</td>
<td class="text-center">{{ $krt->satuan }}</td>
<td class="text-center">
<a href="{{ route('editkriteria', $krt->id) }}" class="btn btn-sm btn-warning">Edit</a>
<form action="{{ url('/kriteria', $krt->id) }}" method="POST" style="display: inline;">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-sm btn-danger">Hapus</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
<!-- End Table with stripped rows -->
<!-- Criteria Table -->
<div class="table-responsive">
<table class="admin-table">
<thead>
<tr>
<th>No</th>
<th>Name</th>
<th>Unit</th>
<th class="text-center">Actions</th>
</tr>
</thead>
<tbody>
@foreach ($kriterias as $krt)
<tr>
<td>{{ ($kriterias->currentPage() - 1) * $kriterias->perPage() + $loop->iteration }}</td>
<td>
<div class="d-flex align-items-center">
<div class="feature-icon bg-primary bg-gradient me-3" style="width: 32px; height: 32px; font-size: 14px;">
<i class="fas fa-check-circle"></i>
</div>
{{ $krt->nama }}
</div>
</td>
<td>
<span class="admin-badge bg-info">
{{ $krt->satuan }}
</span>
</td>
<td class="text-center">
<a href="{{ route('editkriteria', $krt->id) }}"
class="admin-btn btn-warning btn-sm me-2">
<i class="fas fa-edit"></i>
</a>
<form action="{{ url('/kriteria', $krt->id) }}"
method="POST"
class="d-inline"
onsubmit="return confirm('Are you sure you want to delete this criteria?');">
@csrf
@method('DELETE')
<button type="submit" class="admin-btn btn-danger btn-sm">
<i class="fas fa-trash"></i>
</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
<!-- Pagination -->
<div class="d-flex justify-content-between align-items-center mt-4">
<div class="text-muted">
Showing {{ $kriterias->firstItem() ?? 0 }} to {{ $kriterias->lastItem() ?? 0 }} of {{ $kriterias->total() }} entries
</div>
<div class="admin-pagination">
{{ $kriterias->links() }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
@endsection
</div>
@endsection
@push('styles')
<style>
.admin-pagination nav {
margin-bottom: 0;
}
.admin-pagination .pagination {
margin-bottom: 0;
}
.admin-pagination .page-link {
padding: 0.375rem 0.75rem;
font-size: 0.9rem;
border-radius: 0.25rem;
margin: 0 0.125rem;
color: var(--primary-color);
background-color: var(--white);
border: 1px solid var(--border-color);
}
.admin-pagination .page-item.active .page-link {
background-color: var(--primary-color);
border-color: var(--primary-color);
color: var(--white);
}
.admin-pagination .page-item.disabled .page-link {
color: var(--gray);
pointer-events: none;
background-color: var(--light);
border-color: var(--border-color);
}
</style>
@endpush
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
const animateElements = document.querySelectorAll('.animate-fade-in');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.opacity = 1;
entry.target.style.transform = 'translateY(0)';
}
});
});
animateElements.forEach(element => {
element.style.opacity = 0;
element.style.transform = 'translateY(20px)';
observer.observe(element);
});
});
</script>
@endpush

View File

@ -1,74 +1,198 @@
@extends('layout.app')
@section('title', 'Tambah Kriteria')
@include('admin.shared.admin-styles')
@section('content')
<div class="pagetitle">
<div class="admin-container container-fluid">
<!-- Page Header -->
<div class="page-header animate-fade-in">
<div class="row align-items-center">
<div class="col-12">
<h3 class="mb-2 text-white">
<i class="fas fa-plus me-2"></i>Tambah Kriteria
</h3>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}" class="text-white-50">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{{ route('kriteria') }}" class="text-white-50">Kriteria</a></li>
<li class="breadcrumb-item active text-white" aria-current="page">Tambah Kriteria</li>
</ol>
</nav>
</div>
</div>
</div>
<h1>Tambah Data User</h1>
<nav>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}">Home</a></li>
<li class="breadcrumb-item active"><a href="{{ route('kriteria') }}">Kriteria</a></li>
<li class="breadcrumb-item active"><a href="{{ route('tambahkriteria') }}">Tambah Kriteria</a></li>
</ol>
</nav>
<div class="row">
<div class="col-12">
<div class="admin-card animate-fade-in">
<div class="card-body">
<h5 class="card-title mb-4">
<i class="fas fa-list-check me-2"></i>Detail Kriteria
</h5>
</div><!-- End Page Title -->
<form id="AddUserForm" method="POST" action="{{ url('/kriteria/storekriteria') }}">
@csrf
<div class="card">
<div class="card-body">
<h5 class="card-title">Tambah Kriteria</h5>
<div class="row g-4">
<div class="col-md-6">
<div class="admin-form-group">
<label class="admin-form-label">
<i class="fas fa-list-check me-2"></i>Nama Kriteria
</label>
<input type="text"
class="admin-form-control @error('nama') is-invalid @enderror"
name="nama"
value="{{ old('nama') }}"
placeholder="Masukkan nama kriteria"
required>
@error('nama')
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
</div>
<!-- General Form Elements -->
<form id="AddUserForm" method="POST" action="{{ url('/kriteria/storekriteria') }}">
@csrf <!-- Add this to include CSRF token -->
<div class="row mb-3">
<label for="inputText" class="col-sm-2 col-form-label">Kriteria</label>
<div class="col-sm-10">
<input type="text" class="form-control" placeholder="kriteria yang mau dipakai" aria-label="Your Name"
aria-describedby="basic-addon1" name="nama">
</div>
</div>
<div class="row mb-3">
<label for="inputText" class="col-sm-2 col-form-label">Satuan</label>
<div class="col-sm-10">
<input type="text" class="form-control" placeholder="satuan yang mau dipakai" aria-label="Your Name"
aria-describedby="basic-addon1" name="satuan">
<div class="col-md-6">
<div class="admin-form-group">
<label class="admin-form-label">
<i class="fas fa-ruler me-2"></i>Satuan Ukur
</label>
<input type="text"
class="admin-form-control @error('satuan') is-invalid @enderror"
name="satuan"
value="{{ old('satuan') }}"
placeholder="Masukkan satuan ukur"
required>
@error('satuan')
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
</div>
</div>
<div class="text-end mt-4">
<button type="reset" class="btn btn-secondary me-2">
<i class="fas fa-undo me-2"></i>Reset
</button>
<a href="{{ route('kriteria') }}" class="btn btn-secondary me-2">
<i class="fas fa-arrow-left me-2"></i>Kembali
</a>
<button type="button" class="btn btn-primary" onclick="confirmSave()">
<i class="fas fa-save me-2"></i>Simpan
</button>
</div>
</form>
</div>
</div>
<div class="row mb-3">
<div class="col-sm-12 text-center">
<a href="{{ route('kriteria') }}" class="btn btn-secondary mx-3">Back</a>
<button type="button" class="btn btn-primary mx-3" onclick="confirmSave()">Save</button>
</div>
</div>
</form><!-- End General Form Elements -->
</div>
</div>
</div>
</div>
</div>
<style>
.admin-form-group {
margin-bottom: 1.5rem;
}
.admin-form-label {
display: flex;
align-items: center;
font-weight: 500;
margin-bottom: 0.5rem;
color: #333;
}
.admin-form-label i {
width: 20px;
color: #2196f3;
}
.admin-form-control {
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 0.75rem 1rem;
width: 100%;
transition: all 0.3s ease;
}
.admin-form-control:focus {
border-color: #2196f3;
box-shadow: 0 0 0 0.2rem rgba(33, 150, 243, 0.25);
}
.admin-form-control.is-invalid {
border-color: #dc3545;
padding-right: calc(1.5em + 0.75rem);
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right calc(0.375em + 0.1875rem) center;
background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
}
.invalid-feedback {
display: none;
width: 100%;
margin-top: 0.25rem;
font-size: 0.875em;
color: #dc3545;
}
.is-invalid ~ .invalid-feedback {
display: block;
}
/* Responsive Adjustments */
@media (max-width: 768px) {
.admin-form-control {
padding: 0.625rem 0.875rem;
}
}
</style>
@endsection
@push('scripts')
<!-- SweetAlert2 -->
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<!-- Confirmation Dialog Script -->
<script>
function confirmSave() {
function confirmSave() {
Swal.fire({
title: 'Are you sure?',
text: "Do you want to save the changes?",
title: 'Simpan Kriteria',
text: "Apakah Anda yakin ingin menyimpan kriteria ini?",
icon: 'question',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Yes, save it!'
confirmButtonColor: '#2196f3',
cancelButtonColor: '#6c757d',
confirmButtonText: '<i class="fas fa-save me-2"></i>Ya, simpan!',
cancelButtonText: '<i class="fas fa-times me-2"></i>Batal'
}).then((result) => {
if (result.isConfirmed) {
console.log("Form submitted"); // Tambahkan ini di dalam `if (result.isConfirmed)` sebelum submit form
document.getElementById('AddUserForm').submit();
}
});
}
}
document.addEventListener('DOMContentLoaded', function() {
const animateElements = document.querySelectorAll('.animate-fade-in');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.opacity = 1;
entry.target.style.transform = 'translateY(0)';
}
});
});
animateElements.forEach(element => {
element.style.opacity = 0;
element.style.transform = 'translateY(20px)';
observer.observe(element);
});
});
</script>
@endsection
@endpush

View File

@ -1,120 +1,292 @@
@extends('layout.app')
@section('title', 'Edit Makanan')
@include('admin.shared.admin-styles')
@section('content')
<div class="pagetitle">
<h1>Edit Data Makanan</h1>
<nav>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}">Home</a></li>
<li class="breadcrumb-item active"><a href="{{ route('makanan') }}">Makanan></li>
<li class="breadcrumb-item active"><a href="{{ route('editmakanan', $makanan->id) }}">Edit Makanan</a></li>
</ol>
</nav>
</div><!-- End Page Title -->
<div class="card">
<div class="card-body">
<h5 class="card-title">Edit Makanan</h5>
<!-- General Form Elements -->
<form id="editUserForm" method="POST" action="{{ url("/makanan/{$makanan->id}") }}">
@method('PUT')
@csrf
<div class="row mb-3">
<label for="inputText" class="col-sm-2 col-form-label">Name</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="nama" value="{{ $makanan->nama }}">
</div>
</div>
<div class="row mb-3">
<label for="role" class="col-sm-2 col-form-label">Kategori</label>
<div class="col-sm-10">
<select id="role" class="form-select" name="kategori_id">
@foreach ($kategoris as $id => $kategori)
<option value="{{ $id }}" {{ $makanan->kategori_id == $id ? 'selected' : '' }}>{{ $kategori }}</option>
@endforeach
</select>
</div>
</div>
<div class="row mb-3">
<label for="role" class="col-sm-2 col-form-label">Jenis Makanan</label>
<div class="col-sm-10">
<select id="role" class="form-select" name="jenis_id">
@foreach ($jenis_makanans as $id => $jenis)
<option value="{{ $id }}" {{ $makanan->jenis_id == $id ? 'selected' : '' }}>{{ $jenis }}</option>
@endforeach
</select>
</div>
</div>
<div class="row mb-3">
<label for="inputText" class="col-sm-2 col-form-label">Lemak</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="lemak" value="{{ $makanan->lemak }}">
</div>
</div>
<div class="row mb-3">
<label for="inputText" class="col-sm-2 col-form-label">Natrium</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="natrium" value="{{ $makanan->natrium }}">
</div>
</div>
<div class="row mb-3">
<label for="inputText" class="col-sm-2 col-form-label">Energi</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="energi" value="{{ $makanan->energi }}">
</div>
</div>
<div class="row mb-3">
<label for="inputText" class="col-sm-2 col-form-label">Karbohidrat</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="karbohidrat" value="{{ $makanan->karbohidrat }}">
</div>
</div>
<div class="row mb-3">
<div class="col-sm-12 text-center">
<a href="{{ route('makanan') }}" class="btn btn-secondary mx-3">Back</a>
<button type="button" class="btn btn-primary mx-3" onclick="confirmSave()">Save</button>
{{-- <button type="submit" class="btn btn-primary mx-3" onclick="return confirmSave()">Save</button> --}}
<!-- Reset button to reset form fields -->
<button type="reset" class="btn btn-secondary">Reset</button>
</div>
</div>
</form><!-- End General Form Elements -->
<div class="admin-container container-fluid">
<!-- Page Header -->
<div class="page-header animate-fade-in">
<div class="row align-items-center">
<div class="col-12">
<h3 class="mb-2 text-white">
<i class="fas fa-utensils me-2"></i>Edit Makanan
</h3>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}" class="text-white-50">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{{ route('makanan') }}" class="text-white-50">Makanan</a></li>
<li class="breadcrumb-item active text-white" aria-current="page">Edit Makanan</li>
</ol>
</nav>
</div>
</div>
</div>
<!-- SweetAlert2 -->
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<div class="row">
<div class="col-12">
<div class="admin-card animate-fade-in">
<div class="card-body">
<h5 class="card-title mb-4">
<i class="fas fa-utensils me-2"></i>Detail Makanan
</h5>
<!-- Confirmation Dialog Script -->
<script>
function togglePasswordVisibility() {
var passwordField = document.getElementById("passwordField");
if (passwordField.type === "password") {
passwordField.type = "text";
document.querySelector('button[onclick="togglePasswordVisibility()"]').textContent = "Hide";
} else {
passwordField.type = "password";
document.querySelector('button[onclick="togglePasswordVisibility()"]').textContent = "Show";
}
}
<form id="editUserForm" method="POST" action="{{ url("/makanan/{$makanan->id}") }}">
@method('PUT')
@csrf
<div class="row g-4">
<div class="col-md-6">
<div class="admin-form-group">
<label class="admin-form-label">
<i class="fas fa-tag me-2"></i>Nama Makanan
</label>
<input type="text"
class="admin-form-control @error('nama') is-invalid @enderror"
name="nama"
value="{{ old('nama', $makanan->nama) }}"
placeholder="Masukkan nama makanan"
required>
@error('nama')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div class="col-md-6">
<div class="admin-form-group">
<label class="admin-form-label">
<i class="fas fa-list me-2"></i>Kategori
</label>
<select class="admin-form-control @error('kategori_id') is-invalid @enderror"
name="kategori_id"
required>
<option value="" disabled>Pilih Kategori</option>
@foreach($kategoris as $id => $kategori)
<option value="{{ $id }}" {{ old('kategori_id', $makanan->kategori_id) == $id ? 'selected' : '' }}>
{{ $kategori }}
</option>
@endforeach
</select>
@error('kategori_id')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div class="col-md-6">
<div class="admin-form-group">
<label class="admin-form-label">
<i class="fas fa-cookie-bite me-2"></i>Jenis Makanan
</label>
<select class="admin-form-control @error('jenis_id') is-invalid @enderror"
name="jenis_id"
required>
<option value="" disabled>Pilih Jenis Makanan</option>
@foreach($jenis_makanans as $id => $jenis)
<option value="{{ $id }}" {{ old('jenis_id', $makanan->jenis_id) == $id ? 'selected' : '' }}>
{{ $jenis }}
</option>
@endforeach
</select>
@error('jenis_id')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div class="col-md-6">
<div class="admin-form-group">
<label class="admin-form-label">
<i class="fas fa-oil-can me-2"></i>Lemak (g)
</label>
<input type="number"
step="0.01"
class="admin-form-control @error('lemak') is-invalid @enderror"
name="lemak"
value="{{ old('lemak', $makanan->lemak) }}"
placeholder="Masukkan kandungan lemak"
required>
@error('lemak')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div class="col-md-6">
<div class="admin-form-group">
<label class="admin-form-label">
<i class="fas fa-salt-shaker me-2"></i>Natrium (mg)
</label>
<input type="number"
step="0.01"
class="admin-form-control @error('natrium') is-invalid @enderror"
name="natrium"
value="{{ old('natrium', $makanan->natrium) }}"
placeholder="Masukkan kandungan natrium"
required>
@error('natrium')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div class="col-md-6">
<div class="admin-form-group">
<label class="admin-form-label">
<i class="fas fa-bolt me-2"></i>Energi (kkal)
</label>
<input type="number"
step="0.01"
class="admin-form-control @error('energi') is-invalid @enderror"
name="energi"
value="{{ old('energi', $makanan->energi) }}"
placeholder="Masukkan kandungan energi"
required>
@error('energi')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div class="col-md-6">
<div class="admin-form-group">
<label class="admin-form-label">
<i class="fas fa-bread-slice me-2"></i>Karbohidrat (g)
</label>
<input type="number"
step="0.01"
class="admin-form-control @error('karbohidrat') is-invalid @enderror"
name="karbohidrat"
value="{{ old('karbohidrat', $makanan->karbohidrat) }}"
placeholder="Masukkan kandungan karbohidrat"
required>
@error('karbohidrat')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
</div>
<div class="text-end mt-4">
<button type="reset" class="btn btn-secondary me-2">
<i class="fas fa-undo me-2"></i>Reset
</button>
<a href="{{ route('makanan') }}" class="btn btn-secondary me-2">
<i class="fas fa-arrow-left me-2"></i>Kembali
</a>
<button type="button" class="btn btn-primary" onclick="confirmSave()">
<i class="fas fa-save me-2"></i>Simpan Perubahan
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<style>
.admin-form-group {
margin-bottom: 1.5rem;
}
.admin-form-label {
display: flex;
align-items: center;
font-weight: 500;
margin-bottom: 0.5rem;
color: #333;
}
.admin-form-label i {
width: 20px;
color: #2196f3;
}
.admin-form-control {
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 0.75rem 1rem;
width: 100%;
transition: all 0.3s ease;
}
.admin-form-control:focus {
border-color: #2196f3;
box-shadow: 0 0 0 0.2rem rgba(33, 150, 243, 0.25);
}
.admin-form-control.is-invalid {
border-color: #dc3545;
padding-right: calc(1.5em + 0.75rem);
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right calc(0.375em + 0.1875rem) center;
background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
}
.invalid-feedback {
display: none;
width: 100%;
margin-top: 0.25rem;
font-size: 0.875em;
color: #dc3545;
}
.is-invalid ~ .invalid-feedback {
display: block;
}
/* Responsive Adjustments */
@media (max-width: 768px) {
.admin-form-control {
padding: 0.625rem 0.875rem;
}
}
</style>
function confirmSave() {
Swal.fire({
title: 'Are you sure?',
text: "Do you want to save the changes?",
icon: 'question',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Yes, save it!'
}).then((result) => {
if (result.isConfirmed) {
document.getElementById('editUserForm').submit();
}
});
}
</script>
@endsection
@push('scripts')
<!-- SweetAlert2 -->
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script>
function confirmSave() {
Swal.fire({
title: 'Simpan Perubahan',
text: "Apakah Anda yakin ingin menyimpan perubahan ini?",
icon: 'question',
showCancelButton: true,
confirmButtonColor: '#2196f3',
cancelButtonColor: '#6c757d',
confirmButtonText: '<i class="fas fa-save me-2"></i>Ya, simpan!',
cancelButtonText: '<i class="fas fa-times me-2"></i>Batal'
}).then((result) => {
if (result.isConfirmed) {
document.getElementById('editUserForm').submit();
}
});
}
document.addEventListener('DOMContentLoaded', function() {
const animateElements = document.querySelectorAll('.animate-fade-in');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.opacity = 1;
entry.target.style.transform = 'translateY(0)';
}
});
});
animateElements.forEach(element => {
element.style.opacity = 0;
element.style.transform = 'translateY(20px)';
observer.observe(element);
});
});
</script>
@endpush

View File

@ -1,89 +1,181 @@
@extends('layout.app')
@section('title', 'Food Management')
@include('admin.shared.admin-styles')
@section('content')
<div class="pagetitle">
<div class="admin-container container-fluid">
<!-- Page Header -->
<div class="page-header animate-fade-in">
<div class="row align-items-center">
<div class="col-12">
<h3 class="mb-2 text-white">
<i class="fas fa-utensils me-2"></i>Food Management
</h3>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}" class="text-white-50">Dashboard</a></li>
<li class="breadcrumb-item active text-white" aria-current="page">Foods</li>
</ol>
</nav>
</div>
</div>
</div>
<h1>Data Makanan</h1>
<nav>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}">Home</a></li>
<li class="breadcrumb-item active"><a href="{{ route('makanan') }}"> Data makanan</a></li>
</ol>
</nav>
</div><!-- End Page Title -->
<section class="section">
<div class="row">
<div class="col-lg-12">
<div class="card">
<div class="col-12">
<div class="admin-card animate-fade-in">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<h5 class="card-title mb-2">Data Makanan</h5>
<div class="text-center">
<a href="{{ route('tambahmakanan') }}" class="btn btn-primary">Tambah</a>
<!-- Header with Add Button -->
<div class="d-flex justify-content-between align-items-center mb-4">
<h5 class="card-title mb-0">
<i class="fas fa-list me-2"></i>Food List
</h5>
<a href="{{ route('tambahmakanan') }}" class="admin-btn btn-primary">
<i class="fas fa-plus me-2"></i>Add New Food
</a>
</div>
<!-- Important Notice -->
<div class="admin-alert alert-info mb-4 animate-fade-in">
<div class="d-flex align-items-center">
<div class="feature-icon bg-info bg-gradient me-3" style="width: 48px; height: 48px;">
<i class="fas fa-info-circle"></i>
</div>
<div>
<h6 class="fw-bold mb-1">Important Notice</h6>
<p class="mb-0">Komposisi gizi pangan dihitung per <code>100 grams</code>, dengan Berat Dapat Dimakan (BDD) 100%.</p>
</div>
</div>
</div>
<style>
.accordion-button {
font-size: 1.3rem; /* Ukuran font lebih besar */
}
.accordion-body {
font-size: 1.1rem; /* Ukuran font lebih besar */
}
</style>
<div class="accordion-item mb-4">
<h3 class="accordion-header" id="flush-headingOne">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#flush-collapseOne" aria-expanded="false" aria-controls="flush-collapseOne">
<strong> Satuan yang harus di ikuti dan diperhatikan </strong>
</button>
</h3>
<div id="flush-collapseOne" class="accordion-collapse collapse" aria-labelledby="flush-headingOne" data-bs-parent="#accordionFlushExample">
<div class="accordion-body">Komposisi gizi pangan dihitung per <code>100 gram</code>, dengan Berat Dapat Dimakan (BDD) 100 % </div>
<!-- Food Table -->
<div class="table-responsive">
<table class="admin-table">
<thead>
<tr>
<th>No</th>
<th>Name</th>
<th>Kategori</th>
<th>Jenis Makanan</th>
<th>Lemak (g)</th>
<th>Natrium (mg)</th>
<th>Energi (Cal)</th>
<th>Karbohidrat (g)</th>
<th class="text-center">Actions</th>
</tr>
</thead>
<tbody>
@foreach ($makanans as $mknn)
<tr>
<td>{{ ($makanans->currentPage() - 1) * $makanans->perPage() + $loop->iteration }}</td>
<td>
<div class="d-flex align-items-center">
{{ $mknn->nama }}
</div>
</td>
<td>
<span class="admin-badge bg-info">
{{ $mknn->kategori->kategori }}
</span>
</td>
<td>
<span class="admin-badge bg-secondary">
{{ $mknn->jenis->name }}
</span>
</td>
<td>{{ $mknn->lemak }}</td>
<td>{{ $mknn->natrium }}</td>
<td>{{ $mknn->energi }}</td>
<td>{{ $mknn->karbohidrat }}</td>
<td class="text-center">
<a href="{{ route('editmakanan', $mknn->id) }}"
class="admin-btn btn-warning btn-sm me-2">
<i class="fas fa-edit"></i>
</a>
<form action="{{ url('/makanan', $mknn->id) }}"
method="POST"
class="d-inline"
onsubmit="return confirm('Are you sure you want to delete this food item?');">
@csrf
@method('DELETE')
<button type="submit" class="admin-btn btn-danger btn-sm">
<i class="fas fa-trash"></i>
</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
<!-- Pagination -->
<div class="d-flex justify-content-between align-items-center mt-4">
<div class="text-muted">
Showing {{ $makanans->firstItem() ?? 0 }} to {{ $makanans->lastItem() ?? 0 }} of {{ $makanans->total() }} entries
</div>
<div class="admin-pagination">
{{ $makanans->links() }}
</div>
</div>
</div>
<!-- Table with stripped rows -->
<table class="table datatable">
<thead>
<tr>
<th class="text-center">No</th>
<th class="text-center">Name</th>
<th class="text-center">Kategori</th>
<th class="text-center">Jenis Makanan</th>
<th class="text-center">Lemak (g)</th>
<th class="text-center">Natrium (mg)</th>
<th class="text-center">Energi (Kal)</th>
<th class="text-center">Karbohidrat (g)</th>
<th class="text-center">Action</th>
</tr>
</thead>
<tbody>
@foreach ($makanans as $mknn)
<tr>
<td class="text-center">{{ $loop->iteration }}</td>
<td class="text-center">{{ $mknn->nama }}</td>
<td class="text-center">{{ $mknn->kategori->kategori }}</td>
<td class="text-center">{{ $mknn->jenis->name }}</td>
<td class="text-center">{{ $mknn->lemak }}</td>
<td class="text-center">{{ $mknn->natrium }}</td>
<td class="text-center">{{ $mknn->energi }}</td>
<td class="text-center">{{ $mknn->karbohidrat }}</td>
<td class="text-center">
<a href="{{ route('editmakanan', $mknn->id) }}" class="btn btn-sm btn-warning">Edit</a>
<form action="{{ url('/makanan', $mknn->id) }}" method="POST" style="display: inline;">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-sm btn-danger">Hapus</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
<!-- End Table with stripped rows -->
</div>
</div>
</div>
</div>
</section>
@endsection
</div>
@endsection
@push('styles')
<style>
.admin-pagination nav {
margin-bottom: 0;
}
.admin-pagination .pagination {
margin-bottom: 0;
}
.admin-pagination .page-link {
padding: 0.375rem 0.75rem;
font-size: 0.9rem;
border-radius: 0.25rem;
margin: 0 0.125rem;
color: var(--primary-color);
background-color: var(--white);
border: 1px solid var(--border-color);
}
.admin-pagination .page-item.active .page-link {
background-color: var(--primary-color);
border-color: var(--primary-color);
color: var(--white);
}
.admin-pagination .page-item.disabled .page-link {
color: var(--gray);
pointer-events: none;
background-color: var(--light);
border-color: var(--border-color);
}
</style>
@endpush
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
const animateElements = document.querySelectorAll('.animate-fade-in');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.opacity = 1;
entry.target.style.transform = 'translateY(0)';
}
});
});
animateElements.forEach(element => {
element.style.opacity = 0;
element.style.transform = 'translateY(20px)';
observer.observe(element);
});
});
</script>
@endpush

View File

@ -1,118 +1,291 @@
@extends('layout.app')
@section('title', 'Tambah Makanan')
@include('admin.shared.admin-styles')
@section('content')
<div class="pagetitle">
<h1>Tambah Data makanan</h1>
<nav>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}">Home</a></li>
<li class="breadcrumb-item active"><a href="{{ route('makanan') }}">Makanan</a></li>
<li class="breadcrumb-item active"><a href="{{ route('tambahmakanan') }}">Tambah Makanan</a></li>
</ol>
</nav>
</div><!-- End Page Title -->
<div class="card">
<div class="card-body">
<h5 class="card-title">Tambah Makanan</h5>
<!-- General Form Elements -->
<form id="AddUserForm" method="POST" action="{{ url('/makanan/storemakanan') }}">
@csrf <!-- Add this to include CSRF token -->
<div class="row mb-3">
<label for="inputText" class="col-sm-2 col-form-label">Name</label>
<div class="col-sm-10">
<input type="text" class="form-control" placeholder="nama" aria-label="Your Name"
aria-describedby="basic-addon1" name="nama">
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label">Kategori</label>
<div class="col-sm-10">
<select class="form-select" name="kategori_id" aria-label="Select Kategori" required>
<option selected disabled>Select a Kategori</option>
@foreach($kategoris as $kategori)
<option value="{{ $kategori->id }}">{{ $kategori->kategori }}</option>
@endforeach
</select>
<div class="admin-container container-fluid">
<!-- Page Header -->
<div class="page-header animate-fade-in">
<div class="row align-items-center">
<div class="col-12">
<h3 class="mb-2 text-white">
<i class="fas fa-utensils me-2"></i>Tambah Makanan
</h3>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}" class="text-white-50">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{{ route('makanan') }}" class="text-white-50">Makanan</a></li>
<li class="breadcrumb-item active text-white" aria-current="page">Tambah Makanan</li>
</ol>
</nav>
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label">Jenis Makanan</label>
<div class="col-sm-10">
<select class="form-select" name="jenis_id" aria-label="Select Jenis Makanan" required>
<option selected disabled>Select a Jenis Makanan</option>
@foreach($jenis_makanans as $jenis)
<option value="{{ $jenis->id }}">{{ $jenis->name }}</option>
@endforeach
</select>
</div>
</div>
<div class="row mb-3">
<label for="inputText" class="col-sm-2 col-form-label">Lemak</label>
<div class="col-sm-10">
<input type="text" class="form-control" placeholder="lemak" aria-label="Your Name"
aria-describedby="basic-addon1" name="lemak">
</div>
</div>
<div class="row">
<div class="col-12">
<div class="admin-card animate-fade-in">
<div class="card-body">
<h5 class="card-title mb-4">
<i class="fas fa-utensils me-2"></i>Detail Makanan
</h5>
<form id="AddUserForm" method="POST" action="{{ url('/makanan/storemakanan') }}">
@csrf
<div class="row g-4">
<div class="col-md-6">
<div class="admin-form-group">
<label class="admin-form-label">
<i class="fas fa-tag me-2"></i>Nama Makanan
</label>
<input type="text"
class="admin-form-control @error('nama') is-invalid @enderror"
name="nama"
value="{{ old('nama') }}"
placeholder="Masukkan nama makanan"
required>
@error('nama')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div class="col-md-6">
<div class="admin-form-group">
<label class="admin-form-label">
<i class="fas fa-list me-2"></i>Kategori
</label>
<select class="admin-form-control @error('kategori_id') is-invalid @enderror"
name="kategori_id"
required>
<option value="" selected disabled>Pilih Kategori</option>
@foreach($kategoris as $kategori)
<option value="{{ $kategori->id }}" {{ old('kategori_id') == $kategori->id ? 'selected' : '' }}>
{{ $kategori->kategori }}
</option>
@endforeach
</select>
@error('kategori_id')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div class="col-md-6">
<div class="admin-form-group">
<label class="admin-form-label">
<i class="fas fa-cookie-bite me-2"></i>Jenis Makanan
</label>
<select class="admin-form-control @error('jenis_id') is-invalid @enderror"
name="jenis_id"
required>
<option value="" selected disabled>Pilih Jenis Makanan</option>
@foreach($jenis_makanans as $jenis)
<option value="{{ $jenis->id }}" {{ old('jenis_id') == $jenis->id ? 'selected' : '' }}>
{{ $jenis->name }}
</option>
@endforeach
</select>
@error('jenis_id')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div class="col-md-6">
<div class="admin-form-group">
<label class="admin-form-label">
<i class="fas fa-oil-can me-2"></i>Lemak (g)
</label>
<input type="number"
step="0.01"
class="admin-form-control @error('lemak') is-invalid @enderror"
name="lemak"
value="{{ old('lemak') }}"
placeholder="Masukkan kandungan lemak"
required>
@error('lemak')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div class="col-md-6">
<div class="admin-form-group">
<label class="admin-form-label">
<i class="fas fa-salt-shaker me-2"></i>Natrium (mg)
</label>
<input type="number"
step="0.01"
class="admin-form-control @error('natrium') is-invalid @enderror"
name="natrium"
value="{{ old('natrium') }}"
placeholder="Masukkan kandungan natrium"
required>
@error('natrium')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div class="col-md-6">
<div class="admin-form-group">
<label class="admin-form-label">
<i class="fas fa-bolt me-2"></i>Energi (kkal)
</label>
<input type="number"
step="0.01"
class="admin-form-control @error('energi') is-invalid @enderror"
name="energi"
value="{{ old('energi') }}"
placeholder="Masukkan kandungan energi"
required>
@error('energi')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div class="col-md-6">
<div class="admin-form-group">
<label class="admin-form-label">
<i class="fas fa-bread-slice me-2"></i>Karbohidrat (g)
</label>
<input type="number"
step="0.01"
class="admin-form-control @error('karbohidrat') is-invalid @enderror"
name="karbohidrat"
value="{{ old('karbohidrat') }}"
placeholder="Masukkan kandungan karbohidrat"
required>
@error('karbohidrat')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
</div>
<div class="text-end mt-4">
<button type="reset" class="btn btn-secondary me-2">
<i class="fas fa-undo me-2"></i>Reset
</button>
<a href="{{ route('makanan') }}" class="btn btn-secondary me-2">
<i class="fas fa-arrow-left me-2"></i>Kembali
</a>
<button type="button" class="btn btn-primary" onclick="confirmSave()">
<i class="fas fa-save me-2"></i>Simpan
</button>
</div>
</form>
</div>
</div>
<div class="row mb-3">
<label for="inputText" class="col-sm-2 col-form-label">Natrium</label>
<div class="col-sm-10">
<input type="text" class="form-control" placeholder="natrium" aria-label="Your Name"
aria-describedby="basic-addon1" name="natrium">
</div>
</div>
<div class="row mb-3">
<label for="inputText" class="col-sm-2 col-form-label">Energi</label>
<div class="col-sm-10">
<input type="text" class="form-control" placeholder="energi" aria-label="Your Name"
aria-describedby="basic-addon1" name="energi">
</div>
</div>
<div class="row mb-3">
<label for="inputText" class="col-sm-2 col-form-label">Karbohidrat</label>
<div class="col-sm-10">
<input type="text" class="form-control" placeholder="karbohidrat" aria-label="Your Name"
aria-describedby="basic-addon1" name="karbohidrat">
</div>
</div>
</div>
<div class="row mb-3">
<div class="col-sm-12 text-center">
<a href="{{ route('makanan') }}" class="btn btn-secondary mx-3">Back</a>
<button type="button" class="btn btn-primary mx-3" onclick="confirmSave()">Save</button>
</div>
</div>
</form><!-- End General Form Elements -->
</div>
</div>
</div>
</div>
</div>
<style>
.admin-form-group {
margin-bottom: 1.5rem;
}
.admin-form-label {
display: flex;
align-items: center;
font-weight: 500;
margin-bottom: 0.5rem;
color: #333;
}
.admin-form-label i {
width: 20px;
color: #2196f3;
}
.admin-form-control {
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 0.75rem 1rem;
width: 100%;
transition: all 0.3s ease;
}
.admin-form-control:focus {
border-color: #2196f3;
box-shadow: 0 0 0 0.2rem rgba(33, 150, 243, 0.25);
}
.admin-form-control.is-invalid {
border-color: #dc3545;
padding-right: calc(1.5em + 0.75rem);
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right calc(0.375em + 0.1875rem) center;
background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
}
.invalid-feedback {
display: none;
width: 100%;
margin-top: 0.25rem;
font-size: 0.875em;
color: #dc3545;
}
.is-invalid ~ .invalid-feedback {
display: block;
}
/* Responsive Adjustments */
@media (max-width: 768px) {
.admin-form-control {
padding: 0.625rem 0.875rem;
}
}
</style>
@endsection
@push('scripts')
<!-- SweetAlert2 -->
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<!-- Confirmation Dialog Script -->
<script>
function confirmSave() {
function confirmSave() {
Swal.fire({
title: 'Are you sure?',
text: "Do you want to save the changes?",
title: 'Simpan Makanan',
text: "Apakah Anda yakin ingin menyimpan makanan ini?",
icon: 'question',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Yes, save it!'
confirmButtonColor: '#2196f3',
cancelButtonColor: '#6c757d',
confirmButtonText: '<i class="fas fa-save me-2"></i>Ya, simpan!',
cancelButtonText: '<i class="fas fa-times me-2"></i>Batal'
}).then((result) => {
if (result.isConfirmed) {
console.log("Form submitted"); // Tambahkan ini di dalam `if (result.isConfirmed)` sebelum submit form
document.getElementById('AddUserForm').submit();
}
});
}
}
document.addEventListener('DOMContentLoaded', function() {
const animateElements = document.querySelectorAll('.animate-fade-in');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.opacity = 1;
entry.target.style.transform = 'translateY(0)';
}
});
});
animateElements.forEach(element => {
element.style.opacity = 0;
element.style.transform = 'translateY(20px)';
observer.observe(element);
});
});
</script>
@endsection
@endpush

View File

@ -1,168 +1,164 @@
@extends('layout.app')
@section('title', 'Profile')
@include('admin.shared.admin-styles')
@section('content')
<div class="pagetitle">
<h1>Profile</h1>
<nav>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}">Home</a></li>
<li class="breadcrumb-item"><a href="{{ route('admin.profile') }}">Profile</a></li>
</ol>
</nav>
</div>
<section class="section profile">
<div class="row">
<!-- Kartu info ringkas -->
<div class="col-lg-4">
<div class="card border-0 shadow-sm text-center p-4 rounded-4">
<div class="mb-3">
<div class="d-flex justify-content-center">
<div class="rounded-circle bg-white shadow p-4 d-flex align-items-center justify-content-center" style="width: 100px; height: 100px;">
<i class="bi bi-person-fill text-primary fs-1"></i>
</div>
</div>
</div>
<h5 class="fw-semibold mb-1">{{ $user->name }}</h5>
<p class="text-muted mb-2">{{ $user->email }}</p>
<span class="badge rounded-pill bg-primary">
<i class="bi bi-award me-1"></i> {{ ucfirst($user->role->name ?? 'user') }}
</span>
</div>
<div class="admin-container container-fluid">
<!-- Page Header -->
<div class="page-header animate-fade-in">
<div class="row align-items-center">
<div class="col-12">
<h3 class="mb-2 text-white">
<i class="fas fa-user me-2"></i>Profile
</h3>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}" class="text-white-50">Dashboard</a></li>
<li class="breadcrumb-item active text-white" aria-current="page">Profile</li>
</ol>
</nav>
</div>
<!-- Tabs -->
<div class="col-xl-8">
<div class="card">
<div class="card-body pt-3">
<ul class="nav nav-tabs nav-tabs-bordered">
<li class="nav-item">
<button class="nav-link active" data-bs-toggle="tab"
data-bs-target="#tab-overview">Overview</button>
</li>
<li class="nav-item">
<button class="nav-link" data-bs-toggle="tab"
data-bs-target="#tab-edit">Edit Profile</button>
</li>
<li class="nav-item">
<button class="nav-link" data-bs-toggle="tab"
data-bs-target="#tab-password">Change Password</button>
</li>
</ul>
<div class="tab-content pt-2">
<!-- ========= Overview ========= -->
<div class="tab-pane fade show active profile-overview" id="tab-overview">
<h5 class="card-title">Profile Details</h5>
<div class="row mb-3">
<div class="col-lg-3 col-md-4 label">Name</div>
<div class="col-lg-9 col-md-8">{{ $user->name }}</div>
</div>
<div class="row mb-3">
<div class="col-lg-3 col-md-4 label">Email</div>
<div class="col-lg-9 col-md-8">{{ $user->email }}</div>
</div>
<div class="row mb-3">
<div class="col-lg-3 col-md-4 label">Phone</div>
<div class="col-lg-9 col-md-8">{{ $user->no_telp }}</div>
</div>
<div class="row mb-3">
<div class="col-lg-3 col-md-4 label">Role</div>
<div class="col-lg-9 col-md-8">{{ $user->role->name ?? '-' }}</div>
</div>
</div>
<!-- ========= Edit profile ===== -->
<div class="tab-pane fade profile-edit pt-3" id="tab-edit">
<form action="{{ route('profile.update') }}" method="POST">
@csrf @method('PATCH')
<div class="row mb-3">
<label class="col-md-4 col-lg-3 col-form-label">Name</label>
<div class="col-md-8 col-lg-9">
<input name="name" type="text" class="form-control"
value="{{ old('name', $user->name) }}" required>
</div>
</div>
<div class="row mb-3">
<label class="col-md-4 col-lg-3 col-form-label">Email</label>
<div class="col-md-8 col-lg-9">
<input name="email" type="email" class="form-control"
value="{{ old('email', $user->email) }}" required>
</div>
</div>
<div class="row mb-3">
<label class="col-md-4 col-lg-3 col-form-label">Phone</label>
<div class="col-md-8 col-lg-9">
<input name="no_telp" type="phone" class="form-control"
value="{{ old('no_telp', $user->no_telp) }}" required>
</div>
</div>
<div class="row mb-3">
<label class="col-md-4 col-lg-3 col-form-label">Role</label>
<div class="col-md-8 col-lg-9">
<select name="role_id" class="form-control" required>
@foreach (\App\Models\Role::all() as $role)
<option value="{{ $role->id }}" {{ $user->role_id == $role->id ? 'selected' : '' }}>
{{ $role->name }}
</option>
@endforeach
</select>
</div>
</div>
<div class="text-center">
<button type="submit" class="btn btn-primary">Save Changes</button>
</div>
</form>
</div>
<!-- ======== Change password ==== -->
<div class="tab-pane fade pt-3" id="tab-password">
<form action="{{ route('profile.password') }}" method="POST">
@csrf @method('PUT')
<div class="row mb-3">
<label class="col-md-4 col-lg-3 col-form-label">Current Password</label>
<div class="col-md-8 col-lg-9">
<input name="current_password" type="password" class="form-control" required>
</div>
</div>
<div class="row mb-3">
<label class="col-md-4 col-lg-3 col-form-label">New Password</label>
<div class="col-md-8 col-lg-9">
<input name="password" type="password" class="form-control" required>
</div>
</div>
<div class="row mb-3">
<label class="col-md-4 col-lg-3 col-form-label">Confirm Password</label>
<div class="col-md-8 col-lg-9">
<input name="password_confirmation" type="password" class="form-control" required>
</div>
</div>
<div class="text-center">
<button type="submit" class="btn btn-primary">Change Password</button>
</div>
</form>
</div>
</div><!-- tab-content -->
</div>
</div>
</div>
</div>
</section>
<div class="row">
<!-- Profile Card -->
<div class="col-lg-4">
<div class="admin-card animate-fade-in">
<div class="card-body text-center">
<div class="mb-4">
<div class="rounded-circle bg-primary bg-opacity-10 mx-auto p-4 d-flex align-items-center justify-content-center" style="width: 120px; height: 120px;">
<i class="bi bi-person-fill text-primary" style="font-size: 3rem;"></i>
</div>
</div>
<h4 class="fw-bold mb-2">{{ $user->name }}</h4>
<p class="text-muted mb-3">{{ $user->email }}</p>
<div class="admin-badge bg-primary">
<i class="bi bi-award me-1"></i>
{{ ucfirst($user->role->name ?? 'user') }}
</div>
</div>
</div>
</div>
<!-- Profile Details -->
<div class="col-lg-8">
<div class="admin-card animate-fade-in">
<div class="card-body">
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item">
<a class="nav-link active" data-bs-toggle="tab" href="#tab-profile">
<i class="fas fa-user-edit me-2"></i>Edit Profile
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#tab-password">
<i class="fas fa-key me-2"></i>Change Password
</a>
</li>
</ul>
<div class="tab-content pt-4">
<!-- Profile Edit Form -->
<div class="tab-pane fade show active" id="tab-profile">
@if(session('success'))
<div class="admin-alert alert-success" role="alert">
<i class="fas fa-check-circle me-2"></i>{{ session('success') }}
</div>
@endif
@if(session('error'))
<div class="admin-alert alert-danger" role="alert">
<i class="fas fa-exclamation-circle me-2"></i>{{ session('error') }}
</div>
@endif
<form action="{{ route('profile.update') }}" method="POST">
@csrf
@method('PUT')
<div class="admin-form-group row">
<label class="col-md-4 col-lg-3 admin-form-label">Full Name</label>
<div class="col-md-8 col-lg-9">
<input name="name" type="text" class="admin-form-control" value="{{ $user->name }}" required>
</div>
</div>
<div class="admin-form-group row">
<label class="col-md-4 col-lg-3 admin-form-label">Email</label>
<div class="col-md-8 col-lg-9">
<input name="email" type="email" class="admin-form-control" value="{{ $user->email }}" required>
</div>
</div>
<div class="admin-form-group row">
<label class="col-md-4 col-lg-3 admin-form-label">Phone</label>
<div class="col-md-8 col-lg-9">
<input name="no_telp" type="text" class="admin-form-control" value="{{ $user->no_telp }}">
</div>
</div>
<div class="admin-form-group row">
<label class="col-md-4 col-lg-3 admin-form-label">Role</label>
<div class="col-md-8 col-lg-9">
<select name="role_id" class="admin-form-control" required>
@foreach (\App\Models\Role::all() as $role)
<option value="{{ $role->id }}" {{ $user->role_id == $role->id ? 'selected' : '' }}>
{{ $role->name }}
</option>
@endforeach
</select>
</div>
</div>
<div class="text-center">
<button type="submit" class="admin-btn btn-primary">
<i class="fas fa-save me-2"></i>Save Changes
</button>
</div>
</form>
</div>
<!-- Change Password Form -->
<div class="tab-pane fade" id="tab-password">
<form action="{{ route('profile.password') }}" method="POST">
@csrf
@method('PUT')
<div class="admin-form-group row">
<label class="col-md-4 col-lg-3 admin-form-label">Current Password</label>
<div class="col-md-8 col-lg-9">
<input name="current_password" type="password" class="admin-form-control" required>
</div>
</div>
<div class="admin-form-group row">
<label class="col-md-4 col-lg-3 admin-form-label">New Password</label>
<div class="col-md-8 col-lg-9">
<input name="password" type="password" class="admin-form-control" required>
</div>
</div>
<div class="admin-form-group row">
<label class="col-md-4 col-lg-3 admin-form-label">Confirm Password</label>
<div class="col-md-8 col-lg-9">
<input name="password_confirmation" type="password" class="admin-form-control" required>
</div>
</div>
<div class="text-center">
<button type="submit" class="admin-btn btn-primary">
<i class="fas fa-key me-2"></i>Change Password
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@endsection

View File

@ -1,28 +1,46 @@
@extends('layout.app')
@section('content')
@section('title', 'Normalisasi Matriks')
<div class="pagetitle">
<h1>Normalisasi Matriks Perbandingan</h1>
<nav>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}">Home</a></li>
<li class="breadcrumb-item"><a href="{{ route('perbandingan') }}">Perbandingan Kriteria</a></li>
<li class="breadcrumb-item active">Normalisasi Kriteria</li>
@include('admin.shared.admin-styles')
@section('content')
<div class="admin-container container-fluid">
<!-- Page Header -->
<div class="page-header animate-fade-in">
<div class="row align-items-center">
<div class="col-12">
<h3 class="mb-2 text-white">
<i class="fas fa-calculator me-2"></i>Normalisasi Matriks
</h3>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}" class="text-white-50">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{{ route('perbandingan') }}" class="text-white-50">Perbandingan Kriteria</a></li>
<li class="breadcrumb-item active text-white" aria-current="page">Normalisasi Kriteria</li>
</ol>
</nav>
</div>
</div>
</div>
<section class="section">
<div class="row">
<div class="col-lg-12">
<div class="card shadow-lg border-start border-4 border-primary mb-4">
<div class="card-body">
<h5 class="card-title text-primary fw-bold mb-0">
<i class="fas fa-clock me-2"></i>Waktu Makan: {{ $waktu_makan->nama }}
</h5>
</div>
</div>
<!-- Matriks Normalisasi -->
<div class="card">
<div class="col-12">
<div class="admin-card animate-fade-in">
<div class="card-body">
<h5 class="card-title mb-4">Normalisasi Matriks</h5>
<h5 class="card-title mb-4">
<i class="fas fa-table me-2"></i>Normalisasi Matriks
</h5>
<div class="table-responsive">
<table class="table table-bordered text-center">
<table class="admin-table">
<thead>
<tr>
<th>Kriteria</th>
@ -32,26 +50,33 @@
</tr>
</thead>
<tbody>
@foreach ($kriterias as $i => $baris)
<tr>
<th>{{ $baris->nama }}</th>
@foreach ($kriterias as $j => $kolom)
<td>{{ number_format($normalisasi[$i][$j], 4) }}</td>
@endforeach
</tr>
@endforeach
@foreach ($kriterias as $baris)
<tr>
<th>{{ $baris->nama }}</th>
@foreach ($kriterias as $kolom)
<td>
{{ isset($normalisasi[$baris->id][$kolom->id]) ? number_format($normalisasi[$baris->id][$kolom->id], 4) : '-' }}
</td>
@endforeach
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Bobot Kriteria -->
<div class="card mt-4">
<div class="col-12 mt-4">
<div class="admin-card animate-fade-in">
<div class="card-body">
<h5 class="card-title">Bobot Kriteria</h5>
<h5 class="card-title mb-4">
<i class="fas fa-weight me-2"></i>Bobot Kriteria
</h5>
<div class="table-responsive">
<table class="table table-bordered text-center">
<table class="admin-table">
<thead>
<tr>
<th>Kriteria</th>
@ -59,27 +84,84 @@
</tr>
</thead>
<tbody>
@foreach ($kriterias as $index => $kriteria)
<tr>
<td>{{ $kriteria->nama }}</td>
<td>{{ number_format($bobot[$index], 4) }}</td>
</tr>
@endforeach
@foreach ($kriterias as $kriteria)
<tr>
<td>{{ $kriteria->nama }}</td>
<td>{{ number_format($bobot[$kriteria->id], 4) }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Tombol Proses Otomatis -->
<div class="text-end mt-4">
<a href="{{ route('rekomendasi.hitung.otomatis') }}" class="btn btn-success btn-lg">
<i class="bi bi-lightning-fill me-1"></i> Proses Rekomendasi Otomatis
</a>
<!-- Consistency Ratio -->
<div class="col-12 mt-4">
<div class="admin-card animate-fade-in">
<div class="card-body">
<h5 class="card-title mb-4">
<i class="fas fa-check-circle me-2"></i>Rasio Konsistensi
</h5>
<div class="table-responsive">
<table class="admin-table">
<tbody>
<tr>
<th>Consistency Index (CI)</th>
<td>{{ number_format($ci, 2) }}</td>
</tr>
<tr>
<th>Consistency Ratio (CR)</th>
<td>{{ number_format($cr, 2) }}</td>
</tr>
<tr>
<th>Status</th>
<td>
@if($is_consistent)
<span class="badge bg-success">Konsisten (CR < 0.1)</span>
@else
<span class="badge bg-danger">Tidak Konsisten (CR > 0.1)</span>
@endif
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Tombol Lanjut -->
<div class="col-12 mt-4">
<div class="text-end">
<a href="{{ route('alternatif.pilih') }}" class="admin-btn btn-primary">
<i class="fas fa-arrow-right me-2"></i>Lanjut ke Pemilihan Alternatif
</a>
</div>
</div>
</div>
</section>
</div>
@endsection
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
const animateElements = document.querySelectorAll('.animate-fade-in');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.opacity = 1;
entry.target.style.transform = 'translateY(0)';
}
});
});
animateElements.forEach(element => {
element.style.opacity = 0;
element.style.transform = 'translateY(20px)';
observer.observe(element);
});
});
</script>
@endpush

View File

@ -110,6 +110,25 @@
<h5 class="card-title mb-4">Input Perbandingan Kriteria</h5>
<form method="POST" action="{{ route('sementara') }}">
@csrf
<!-- Tambahkan dropdown waktu makan -->
<div class="row mb-4">
<div class="col-md-6">
<div class="form-group">
<label for="waktu_makan_id" class="form-label">Pilih Waktu Makan</label>
<select name="waktu_makan_id" id="waktu_makan_id" class="form-select" required>
<option value="">Pilih Waktu Makan</option>
@foreach(\App\Models\WaktuMakan::all() as $waktu)
<option value="{{ $waktu->id }}" {{ old('waktu_makan_id') == $waktu->id ? 'selected' : '' }}>
{{ $waktu->nama }}
</option>
@endforeach
</select>
@error('waktu_makan_id')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
</div>
<div class="table-responsive">
<table class="table table-bordered text-center">
<thead>
@ -133,6 +152,9 @@
@for ($n = 1; $n <= 9; $n++)
<option value="{{ $n }}">{{ $n }}</option>
@endfor
@for ($n = 2; $n <= 9; $n++)
<option value="{{ 1/$n }}">{{ '1/'.$n }}</option>
@endfor
</select>
@else
<input type="text" class="form-control text-center" value="-" disabled>

View File

@ -0,0 +1,191 @@
@extends('layout.app')
@section('title', 'Daftar Rekomendasi Ahli')
@include('admin.shared.admin-styles')
@section('content')
<div class="admin-container container-fluid">
<!-- Page Header -->
<div class="page-header animate-fade-in">
<div class="row align-items-center">
<div class="col-12">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3">
<div>
<h3 class="mb-1 text-white d-flex align-items-center">
<i class="fas fa-user-md me-2"></i>Daftar Rekomendasi Ahli
</h3>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}" class="text-white-50">Dashboard</a></li>
<li class="breadcrumb-item active text-white" aria-current="page">Rekomendasi Ahli</li>
</ol>
</nav>
</div>
<div class="d-flex gap-2">
<button class="admin-btn btn-primary" onclick="window.location.reload()">
<i class="fas fa-sync-alt me-1"></i>Refresh
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Card per Hari -->
<div class="d-flex overflow-auto gap-4 flex-nowrap px-2" id="cardsContainer">
@foreach($daftarHari as $hari)
<div class="recommendation-card flex-shrink-0" style="min-width: 360px;">
<div class="card-header bg-gradient-primary d-flex justify-content-between align-items-center">
<h5 class="mb-0 text-truncate">
<i class="fas fa-calendar-day me-2"></i>{{ $hari }}
</h5>
<a href="{{ route('rekomendasi_ahli.create', ['hari' => $hari]) }}" class="btn btn-sm btn-light" title="Tambah Rekomendasi">
<i class="fas fa-plus"></i>
</a>
</div>
<div class="card-body">
@if(isset($rekomendasiByHari[$hari]) && $rekomendasiByHari[$hari]->count())
@foreach(
$rekomendasiByHari[$hari]
->sortBy('waktuMakan.urutan') // pastikan ada kolom 'urutan' di waktu_makan
->groupBy('waktu_makan_id') as $waktuMakanId => $groupByWaktu
)
<div class="mb-3">
<span class="badge bg-info mb-1">
{{ $groupByWaktu->first()->waktuMakan->nama ?? '-' }}
</span>
<div class="components-container">
@foreach(
collect($groupByWaktu->groupBy('komponen_id'))
->sortBy(function ($item, $key) {
$order = ['Karbohidrat', 'Protein', 'Sayur', 'Buah', 'Susu', 'Snack'];
return array_search(optional($item->first()->komponen)->nama, $order);
}) as $komponenId => $groupByKomponen
)
<div class="mb-2">
<span class="component-badge">
{{ $groupByKomponen->first()->komponen->nama ?? '-' }}
</span>
<ul class="mb-0 ps-3">
@foreach($groupByKomponen as $rekomendasi)
<li class="d-flex align-items-center justify-content-between">
<span>{{ $rekomendasi->makanan->nama ?? '-' }}</span>
<form action="{{ route('rekomendasi_ahli.destroy', $rekomendasi->id) }}" method="POST" class="ms-2 d-inline-block" onsubmit="return confirm('Yakin ingin menghapus rekomendasi ini?')">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-sm btn-danger" title="Hapus">
<i class="fas fa-trash"></i>
</button>
</form>
</li>
@endforeach
</ul>
</div>
@endforeach
</div>
</div>
@endforeach
@else
<div class="text-muted">Tidak ada rekomendasi untuk hari ini.</div>
@endif
</div>
</div>
@endforeach
</div>
</div>
<style>
@import url('https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@400;700&display=swap');
#cardsContainer {
scroll-snap-type: x mandatory;
padding-bottom: 1rem;
}
.recommendation-card {
scroll-snap-align: start;
width: 360px;
flex-shrink: 0;
}
.stats-overview {
margin-bottom: 2rem;
}
.stat-card {
padding: 1.5rem;
border-radius: 1rem;
color: white;
height: 100%;
display: flex;
align-items: center;
gap: 1.5rem;
transition: transform 0.3s ease;
}
.stat-card:hover {
transform: translateY(-5px);
}
.stat-icon {
font-size: 2.5rem;
opacity: 0.9;
}
.stat-details {
flex-grow: 1;
}
.stat-value {
font-size: 1.75rem;
font-weight: 600;
margin-bottom: 0.25rem;
}
.stat-label {
margin-bottom: 0;
opacity: 0.9;
font-size: 0.9rem;
}
.recommendation-card {
background: white;
border-radius: 1rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
}
.recommendation-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.15);
}
.card-header {
padding: 1.25rem;
color: white;
}
.bg-gradient-primary {
background: linear-gradient(45deg, #2196f3, #64b5f6);
}
.card-body {
padding: 1.25rem;
flex-grow: 1;
}
.section-title {
font-size: 1rem;
font-weight: 600;
margin-bottom: 1rem;
color: #333;
}
.components-container {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.component-badge {
background: #e3f2fd;
color: #1976d2;
padding: 0.35rem 0.75rem;
border-radius: 2rem;
font-size: 0.875rem;
margin-bottom: 0.25rem;
display: inline-block;
}
</style>
@endsection

View File

@ -0,0 +1,62 @@
@extends('layout.app')
@section('title', 'Tambah Rekomendasi Ahli')
@section('content')
<div class="container py-4">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">Tambah Rekomendasi Ahli</h5>
</div>
<div class="card-body">
<form action="{{ route('rekomendasi_ahli.store') }}" method="POST">
@csrf
<div class="mb-3">
<label for="hari" class="form-label">Hari</label>
<select name="hari" id="hari" class="form-select" required>
<option value="">Pilih Hari</option>
@foreach(['Senin','Selasa','Rabu','Kamis','Jumat','Sabtu','Minggu'] as $h)
<option value="{{ $h }}" {{ (old('hari', $hari ?? '') == $h) ? 'selected' : '' }}>{{ $h }}</option>
@endforeach
</select>
</div>
<div class="mb-3">
<label for="waktu_makan_id" class="form-label">Waktu Makan</label>
<select name="waktu_makan_id" id="waktu_makan_id" class="form-select" required>
<option value="">Pilih Waktu Makan</option>
@foreach($waktuMakans as $waktu)
<option value="{{ $waktu->id }}" {{ old('waktu_makan_id') == $waktu->id ? 'selected' : '' }}>{{ $waktu->nama }}</option>
@endforeach
</select>
</div>
<div class="mb-3">
<label for="komponen_id" class="form-label">Komponen</label>
<select name="komponen_id" id="komponen_id" class="form-select" required>
<option value="">Pilih Komponen</option>
@foreach($komponens as $komponen)
<option value="{{ $komponen->id }}" {{ old('komponen_id') == $komponen->id ? 'selected' : '' }}>{{ $komponen->nama }}</option>
@endforeach
</select>
</div>
<div class="mb-3">
<label for="makanan_id" class="form-label">Makanan</label>
<select name="makanan_id" id="makanan_id" class="form-select" required>
<option value="">Pilih Makanan</option>
@foreach($makanans as $makanan)
<option value="{{ $makanan->id }}" {{ old('makanan_id') == $makanan->id ? 'selected' : '' }}>{{ $makanan->nama }}</option>
@endforeach
</select>
</div>
<div class="d-flex justify-content-between">
<a href="{{ route('rekomendasi_ahli.index') }}" class="btn btn-secondary">Kembali</a>
<button type="submit" class="btn btn-primary">Simpan</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection

View File

@ -0,0 +1,63 @@
@extends('layout.app')
@section('title', 'Edit Rekomendasi Ahli')
@section('content')
<div class="container py-4">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">Edit Rekomendasi Ahli</h5>
</div>
<div class="card-body">
<form action="{{ route('rekomendasi_ahli.update', $rekomendasi->id) }}" method="POST">
@csrf
@method('PUT')
<div class="mb-3">
<label for="hari" class="form-label">Hari</label>
<select name="hari" id="hari" class="form-select" required>
<option value="">Pilih Hari</option>
@foreach(['Senin','Selasa','Rabu','Kamis','Jumat','Sabtu','Minggu'] as $h)
<option value="{{ $h }}" {{ (old('hari', $rekomendasi->hari) == $h) ? 'selected' : '' }}>{{ $h }}</option>
@endforeach
</select>
</div>
<div class="mb-3">
<label for="waktu_makan_id" class="form-label">Waktu Makan</label>
<select name="waktu_makan_id" id="waktu_makan_id" class="form-select" required>
<option value="">Pilih Waktu Makan</option>
@foreach($waktuMakans as $waktu)
<option value="{{ $waktu->id }}" {{ old('waktu_makan_id', $rekomendasi->waktu_makan_id) == $waktu->id ? 'selected' : '' }}>{{ $waktu->nama }}</option>
@endforeach
</select>
</div>
<div class="mb-3">
<label for="komponen_id" class="form-label">Komponen</label>
<select name="komponen_id" id="komponen_id" class="form-select" required>
<option value="">Pilih Komponen</option>
@foreach($komponens as $komponen)
<option value="{{ $komponen->id }}" {{ old('komponen_id', $rekomendasi->komponen_id) == $komponen->id ? 'selected' : '' }}>{{ $komponen->nama }}</option>
@endforeach
</select>
</div>
<div class="mb-3">
<label for="makanan_id" class="form-label">Makanan</label>
<select name="makanan_id" id="makanan_id" class="form-select" required>
<option value="">Pilih Makanan</option>
@foreach($makanans as $makanan)
<option value="{{ $makanan->id }}" {{ old('makanan_id', $rekomendasi->makanan_id) == $makanan->id ? 'selected' : '' }}>{{ $makanan->nama }}</option>
@endforeach
</select>
</div>
<div class="d-flex justify-content-between">
<a href="{{ route('rekomendasi_ahli.index') }}" class="btn btn-secondary">Kembali</a>
<button type="submit" class="btn btn-primary">Update</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection

View File

@ -0,0 +1,483 @@
@extends('layout.app')
@section('title', 'Daftar Rekomendasi')
@include('admin.shared.admin-styles')
@section('content')
<div class="admin-container container-fluid">
<!-- Page Header -->
<div class="page-header animate-fade-in">
<div class="row align-items-center">
<div class="col-12">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3">
<div>
<h3 class="mb-1 text-white d-flex align-items-center">
<i class="fas fa-list-alt me-2"></i>Daftar Rekomendasi Makanan
</h3>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}" class="text-white-50">Dashboard</a></li>
<li class="breadcrumb-item active text-white" aria-current="page">Daftar Rekomendasi</li>
</ol>
</nav>
</div>
<div class="d-flex gap-2">
<button class="admin-btn btn-primary" onclick="window.location.reload()">
<i class="fas fa-sync-alt me-1"></i>Refresh
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Stats Overview -->
<div class="stats-overview animate-fade-in">
<div class="row g-3">
<div class="col-12 col-md-4">
<div class="stat-card bg-gradient-success">
<div class="stat-icon">
<i class="fas fa-check-circle"></i>
</div>
<div class="stat-details">
<h4 class="stat-value">{{ $waktuMakans->where('has_recommendation', true)->count() }}</h4>
<p class="stat-label">Waktu Makan Terhitung</p>
</div>
</div>
</div>
<div class="col-12 col-md-4">
<div class="stat-card bg-gradient-warning">
<div class="stat-icon">
<i class="fas fa-clock"></i>
</div>
<div class="stat-details">
<h4 class="stat-value">{{ $waktuMakans->where('has_recommendation', false)->count() }}</h4>
<p class="stat-label">Menunggu Perhitungan</p>
</div>
</div>
</div>
<div class="col-12 col-md-4">
<div class="stat-card bg-gradient-info">
<div class="stat-icon">
<i class="fas fa-calendar-alt"></i>
</div>
<div class="stat-details">
<h4 class="stat-value">{{ $waktuMakans->max('latest_calculation') ? \Carbon\Carbon::parse($waktuMakans->max('latest_calculation'))->format('d M Y') : 'Belum ada' }}</h4>
<p class="stat-label">Terakhir Update</p>
</div>
</div>
</div>
</div>
</div>
<!-- Alerts -->
@if(session('error'))
<div class="admin-alert alert-danger animate-fade-in" role="alert">
<div class="d-flex align-items-center">
<i class="fas fa-exclamation-circle me-2"></i>
<div>{{ session('error') }}</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
@endif
@if(session('success'))
<div class="admin-alert alert-success animate-fade-in" role="alert">
<div class="d-flex align-items-center">
<i class="fas fa-check-circle me-2"></i>
<div>{{ session('success') }}</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
@endif
<!-- Waktu Makan Cards -->
<div class="row g-4" id="cardsContainer">
@foreach($waktuMakans as $waktuMakan)
@php
$isNewlyCalculated = $waktuMakan->latest_calculation &&
\Carbon\Carbon::parse($waktuMakan->latest_calculation)->isToday();
@endphp
<div class="col-12 col-md-6 col-lg-4 animate-fade-in" style="animation-delay: {{ $loop->iteration * 0.1 }}s">
<div class="recommendation-card {{ $isNewlyCalculated ? 'newly-calculated' : '' }}">
<div class="card-header {{ $isNewlyCalculated ? 'bg-gradient-success' : 'bg-gradient-primary' }}">
<div class="d-flex justify-content-between align-items-center">
<h5 class="mb-0 d-flex align-items-center">
<i class="fas fa-clock me-2"></i>
<span class="text-truncate">{{ $waktuMakan->nama }}</span>
</h5>
<div class="status-badge">
@if($waktuMakan->has_recommendation)
<span class="badge bg-light text-dark">
<i class="fas fa-check me-1"></i>Terhitung
</span>
@else
<span class="badge bg-warning">
<i class="fas fa-hourglass-half me-1"></i>Pending
</span>
@endif
</div>
</div>
</div>
<div class="card-body">
<div class="components-section mb-3">
<h6 class="section-title">
<i class="fas fa-utensils me-2"></i>Komponen Makanan
</h6>
<div class="components-container">
@foreach($waktuMakan->komponens as $komponen)
<span class="component-badge">
{{ $komponen->nama }}
</span>
@endforeach
</div>
</div>
@if($waktuMakan->latest_calculation)
<div class="calculation-info">
<div class="info-item">
<i class="fas fa-calendar me-2"></i>
<span class="info-text">
Terakhir dihitung:
{{ \Carbon\Carbon::parse($waktuMakan->latest_calculation)->format('d M Y H:i') }}
@if($isNewlyCalculated)
<span class="badge bg-success ms-2">Baru</span>
@endif
</span>
</div>
@if($waktuMakan->latestConsistencyRatio)
<div class="info-item">
<i class="fas fa-calculator me-2"></i>
<span class="info-text">
CR: {{ number_format($waktuMakan->latestConsistencyRatio->nilai_cr, 3) }}
@if($waktuMakan->latestConsistencyRatio->nilai_cr < 0.1)
<span class="badge bg-success ms-1">Konsisten</span>
@else
<span class="badge bg-warning ms-1">Perlu Review</span>
@endif
</span>
</div>
@endif
</div>
@endif
</div>
<div class="card-footer">
<div class="d-flex flex-wrap gap-2">
@if($waktuMakan->has_recommendation)
<a href="{{ route('rekomendasi.detail', $waktuMakan->id) }}"
class="action-btn btn-primary flex-grow-1">
<i class="fas fa-eye me-1"></i>Detail
</a>
@endif
<a href="{{ route('alternatif.pilih', ['waktu_makan' => $waktuMakan->id]) }}"
class="action-btn {{ $waktuMakan->has_recommendation ? 'btn-outline-primary' : 'btn-primary' }} flex-grow-1">
<i class="fas fa-calculator me-1"></i>
{{ $waktuMakan->has_recommendation ? 'Hitung Ulang' : 'Hitung' }}
</a>
</div>
</div>
</div>
</div>
@endforeach
</div>
</div>
<style>
/* Stats Cards */
.stats-overview {
margin-bottom: 2rem;
}
.stat-card {
padding: 1.5rem;
border-radius: 1rem;
color: white;
height: 100%;
display: flex;
align-items: center;
gap: 1.5rem;
transition: transform 0.3s ease;
}
.stat-card:hover {
transform: translateY(-5px);
}
.stat-icon {
font-size: 2.5rem;
opacity: 0.9;
}
.stat-details {
flex-grow: 1;
}
.stat-value {
font-size: 1.75rem;
font-weight: 600;
margin-bottom: 0.25rem;
}
.stat-label {
margin-bottom: 0;
opacity: 0.9;
font-size: 0.9rem;
}
/* Recommendation Cards */
.recommendation-card {
background: white;
border-radius: 1rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
}
.recommendation-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.15);
}
.recommendation-card.newly-calculated {
border: 2px solid var(--bs-success);
}
.card-header {
padding: 1.25rem;
color: white;
}
.bg-gradient-primary {
background: linear-gradient(45deg, #2196f3, #64b5f6);
}
.bg-gradient-success {
background: linear-gradient(45deg, #4caf50, #81c784);
}
.bg-gradient-warning {
background: linear-gradient(45deg, #ff9800, #ffb74d);
}
.bg-gradient-info {
background: linear-gradient(45deg, #00bcd4, #4dd0e1);
}
.card-body {
padding: 1.25rem;
flex-grow: 1;
}
.section-title {
font-size: 1rem;
font-weight: 600;
margin-bottom: 1rem;
color: #333;
}
.components-container {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.component-badge {
background: #e3f2fd;
color: #1976d2;
padding: 0.35rem 0.75rem;
border-radius: 2rem;
font-size: 0.875rem;
}
.calculation-info {
background: #f8f9fa;
padding: 1rem;
border-radius: 0.5rem;
margin-top: 1rem;
}
.info-item {
display: flex;
align-items: center;
font-size: 0.875rem;
color: #666;
margin-bottom: 0.5rem;
}
.info-item:last-child {
margin-bottom: 0;
}
.card-footer {
padding: 1.25rem;
background: #f8f9fa;
border-top: 1px solid #eee;
}
.action-btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
text-decoration: none;
font-weight: 500;
transition: all 0.3s ease;
}
.action-btn.btn-primary {
background: #2196f3;
color: white;
}
.action-btn.btn-outline-primary {
border: 1px solid #2196f3;
color: #2196f3;
}
.action-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
/* Responsive Adjustments */
@media (max-width: 768px) {
.stat-card {
padding: 1rem;
}
.stat-icon {
font-size: 2rem;
}
.stat-value {
font-size: 1.5rem;
}
.recommendation-card {
margin-bottom: 1rem;
}
.card-header h5 {
font-size: 1rem;
}
.component-badge {
font-size: 0.75rem;
}
.info-item {
font-size: 0.8125rem;
}
}
/* List View Styles */
.list-view .recommendation-card {
flex-direction: row;
align-items: center;
height: auto;
}
.list-view .card-header {
width: 25%;
border-right: 1px solid rgba(255, 255, 255, 0.1);
}
.list-view .card-body {
width: 50%;
padding: 1rem;
}
.list-view .card-footer {
width: 25%;
border-top: none;
border-left: 1px solid #eee;
}
@media (max-width: 991.98px) {
.list-view .recommendation-card {
flex-direction: column;
}
.list-view .card-header,
.list-view .card-body,
.list-view .card-footer {
width: 100%;
}
}
/* Animations */
.animate-fade-in {
opacity: 0;
transform: translateY(20px);
transition: all 0.5s ease;
}
.animate-fade-in.show {
opacity: 1;
transform: translateY(0);
}
</style>
@endsection
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
// Animation Observer
const animateElements = document.querySelectorAll('.animate-fade-in');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('show');
}
});
}, {
threshold: 0.1
});
animateElements.forEach(element => observer.observe(element));
// Toggle View
const toggleViewBtn = document.getElementById('toggleView');
const cardsContainer = document.getElementById('cardsContainer');
let isListView = localStorage.getItem('recommendationViewMode') === 'list';
function updateViewMode() {
if (isListView) {
cardsContainer.classList.add('list-view');
toggleViewBtn.innerHTML = '<i class="fas fa-th-large me-1"></i>Grid View';
} else {
cardsContainer.classList.remove('list-view');
toggleViewBtn.innerHTML = '<i class="fas fa-list me-1"></i>List View';
}
}
updateViewMode();
toggleViewBtn.addEventListener('click', () => {
isListView = !isListView;
localStorage.setItem('recommendationViewMode', isListView ? 'list' : 'grid');
updateViewMode();
});
// Smooth Scroll
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
});
});
});
</script>
@endpush

View File

@ -1,102 +1,610 @@
@extends('layout.app')
@section('title', 'Detail Rekomendasi')
@section('content')
<div class="pagetitle">
<h1>🏆 Ranking Rekomendasi Makanan</h1>
<nav>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}">Home</a></li>
<li class="breadcrumb-item active">Hasil Rekomendasi</li>
</ol>
</nav>
</div>
<section class="section">
<div class="row">
<div class="col-12">
@if(session('success'))
<div class="alert alert-success shadow-sm rounded-3">
<i class="bi bi-check-circle me-2"></i> {{ session('success') }}
<div class="admin-container">
<!-- Page Header -->
<div class="page-header">
<div class="row align-items-center">
<div class="col-12">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3">
<div class="d-flex align-items-center">
<div class="icon-title-page bg-primary text-white me-3">
<i class="fas fa-chart-bar"></i>
</div>
<div>
<h3 class="mb-1">Detail Rekomendasi Makanan</h3>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{{ route('rekomendasi.index') }}">Daftar Rekomendasi</a></li>
<li class="breadcrumb-item active" aria-current="page">Detail</li>
</ol>
</nav>
</div>
</div>
<div>
<a href="{{ route('rekomendasi.index') }}" class="admin-btn btn-secondary">
<i class="fas fa-arrow-left me-2"></i>Kembali
</a>
</div>
</div>
@endif
</div>
</div>
</div>
@forelse ($rekomendasi as $waktu => $komponens)
<div class="card shadow-lg border-start border-4 border-success mt-4">
<div class="card-body">
<h4 class="card-title fw-bold text-success mb-4">
🍽️ Menu {{ ucfirst($waktu) }}
@if(session('success'))
<div class="admin-alert alert-success" role="alert">
<div class="d-flex align-items-center">
<i class="fas fa-check-circle me-2"></i>
<div>{{ session('success') }}</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
@endif
@if(session('error'))
<div class="admin-alert alert-danger" role="alert">
<div class="d-flex align-items-center">
<i class="fas fa-exclamation-circle me-2"></i>
<div>{{ session('error') }}</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
@endif
<!-- Stats Overview -->
<div class="stats-overview mb-4">
<div class="row g-3">
<div class="col-xl-3 col-md-6">
<div class="stat-card bg-gradient-primary h-100">
<div class="stat-icon">
<i class="fas fa-clock"></i>
</div>
<div class="stat-details">
<h4 class="stat-value">{{ $waktuMakan->nama }}</h4>
<p class="stat-label">Waktu Makan</p>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="stat-card bg-gradient-success h-100">
<div class="stat-icon">
<i class="fas fa-list-check"></i>
</div>
<div class="stat-details">
<h4 class="stat-value">{{ count($hasilPerKomponen) }}</h4>
<p class="stat-label">Total Komponen</p>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="stat-card bg-gradient-info h-100">
<div class="stat-icon">
<i class="fas fa-clipboard-list"></i>
</div>
<div class="stat-details">
<h4 class="stat-value">{{ count($kriterias) }}</h4>
<p class="stat-label">Total Kriteria</p>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="stat-card bg-gradient-orange h-100">
<div class="stat-icon">
<i class="fas fa-calculator"></i>
</div>
<div class="stat-details">
<h4 class="stat-value">
@if(isset($latestCR))
<span class="cr-value">{{ number_format($latestCR->cr, 4) }}</span>
<span class="cr-status">({{ $latestCR->is_consistent ? 'Konsisten' : 'Perlu Review' }})</span>
@else
<span class="cr-value">Belum dihitung</span>
@endif
</h4>
<p class="stat-label">Consistency Ratio (CR)</p>
</div>
</div>
</div>
</div>
</div>
@foreach ($komponens as $komponen => $items)
@php
$utama = $items->first();
$alternatif = $items->skip(1)->take(4)->values(); // top 5 total
@endphp
<div class="mb-4">
<h5 class="text-primary fw-semibold">
📌 {{ ucfirst($komponen) }}
</h5>
<div class="border rounded-3 p-3 bg-light shadow-sm mb-2 d-flex justify-content-between align-items-center">
<div>
🌟 <strong>{{ $utama->makanan->nama }}</strong>
<!-- Recommendation Results -->
<div class="recommendation-results">
@foreach($hasilPerKomponen as $komponenId => $hasil)
<div class="recommendation-card mb-4">
<div class="card-header bg-gradient-primary">
<h5 class="mb-0 text-white">
<i class="fas fa-chart-bar me-2"></i>
Hasil Perhitungan - {{ $hasil['komponen']->nama }}
</h5>
</div>
<div class="card-body">
<!-- Kriteria Section -->
<div class="criteria-section mb-4">
<h6 class="section-title mb-3">
<i class="fas fa-list-check me-2"></i>Kriteria yang Digunakan
</h6>
<div class="row g-3">
@foreach($kriterias as $kriteria)
<div class="col-xl-3 col-md-6">
<div class="criteria-card h-100">
<div class="d-flex align-items-center">
<div class="criteria-icon">
<i class="fas fa-check-circle"></i>
</div>
<div class="criteria-details">
<div class="criteria-name">{{ $kriteria->nama }}</div>
@if(isset($bobotKriteria[$kriteria->id]))
<div class="criteria-weight">
Bobot: {{ number_format($bobotKriteria[$kriteria->id], 4) }}
</div>
@endif
</div>
<span class="badge bg-success rounded-pill">
{{ number_format($utama->persentase, 2) }}%
</span>
</div>
</div>
</div>
@endforeach
</div>
</div>
@if ($alternatif->count() > 0)
<button class="btn btn-sm btn-outline-primary toggle-btn mb-2"
type="button"
data-target="#alt-{{ $waktu }}-{{ $komponen }}">
🔁 Lihat Alternatif
</button>
<ol id="alt-{{ $waktu }}-{{ $komponen }}"
class="list-group list-group-numbered shadow-sm d-none">
@foreach ($alternatif as $index => $alt)
<li class="list-group-item d-flex justify-content-between align-items-center">
<div>
@if($index == 0) 🥈 @elseif($index == 1) 🥉 @else 🔹 @endif
<strong>{{ $alt->makanan->nama }}</strong>
</div>
<span class="badge bg-secondary rounded-pill">
{{ number_format($alt->persentase, 2) }}%
</span>
</li>
@endforeach
</ol>
@endif
<!-- Consistency Ratio Section -->
<div class="consistency-section mb-4">
<h6 class="section-title mb-3">
<i class="fas fa-calculator me-2"></i>Consistency Ratio per Kriteria
</h6>
<div class="row g-3">
@foreach($kriterias as $kriteria)
@php
$crData = \App\Models\ConsistencyRatioAlternatif::where([
'kriteria_id' => $kriteria->id,
'komponen_id' => $hasil['komponen']->id,
'waktu_makan_id' => $waktuMakan->id
])->latest()->first();
@endphp
<div class="col-xl-3 col-md-6">
<div class="consistency-card h-100 {{ $crData && $crData->is_consistent ? 'consistent' : 'inconsistent' }}">
<div class="consistency-header">
<h6 class="mb-0">{{ $kriteria->nama }}</h6>
<div class="consistency-icon">
@if($crData && $crData->is_consistent)
<i class="fas fa-check-circle"></i>
@else
<i class="fas fa-exclamation-circle"></i>
@endif
</div>
</div>
<div class="consistency-details">
<div class="detail-item">
<span class="label">CI:</span>
<span class="value">{{ $crData ? number_format($crData->ci, 4) : 'N/A' }}</span>
</div>
<div class="detail-item">
<span class="label">CR:</span>
<span class="value">{{ $crData ? number_format($crData->cr, 4) : 'N/A' }}</span>
</div>
<div class="status-badge {{ $crData && $crData->is_consistent ? 'consistent' : 'inconsistent' }}">
@if($crData && $crData->is_consistent)
<i class="fas fa-check me-1"></i>Konsisten
@else
<i class="fas fa-exclamation-triangle me-1"></i>Perlu Review
@endif
</div>
</div>
</div>
</div>
@endforeach
</div>
</div>
@empty
<div class="alert alert-warning mt-4">
<i class="bi bi-exclamation-circle me-2"></i> Tidak ada data rekomendasi tersedia untuk hari ini.
</div>
@endforelse
<!-- Table Section -->
<div class="table-section">
<div class="table-header bg-gradient-success">
<h6 class="mb-0">
<i class="fas fa-table me-2"></i>
Hasil Normalisasi
</h6>
</div>
<div class="table-content">
<div class="alert alert-info mb-3">
<i class="fas fa-info-circle me-2"></i>
Nilai yang ditampilkan adalah hasil normalisasi dari matriks perbandingan
</div>
<div class="table-responsive">
<table class="table table-bordered table-hover mb-0">
<thead>
<tr>
<th style="width: 60px">No</th>
<th>Makanan</th>
@foreach($kriterias as $kriteria)
<th class="text-center">{{ $kriteria->nama }}</th>
@endforeach
<th class="text-center">Skor Akhir</th>
</tr>
</thead>
<tbody>
@foreach($hasil['rekomendasis'] as $rekomendasi)
<tr>
<td class="text-center">{{ $loop->iteration }}</td>
<td>{{ $rekomendasi->makanan->nama }}</td>
@foreach($kriterias as $kriteria)
<td class="text-center score-cell">
{{ number_format($hasil['detailSkor'][$rekomendasi->makanan_id][$kriteria->nama], 4) }}
</td>
@endforeach
<td class="text-center fw-bold score-cell">
{{ number_format($rekomendasi->nilai_akhir, 4) }}
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
@endforeach
</div>
</section>
</div>
@push('styles')
<style>
/* Base Container */
.admin-container {
padding: 1.5rem;
max-width: 1600px;
margin: 0 auto;
}
/* Page Header */
.page-header {
margin-bottom: 2rem;
background: #fff;
border-radius: 1rem;
padding: 1.5rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.icon-title-page {
width: 45px;
height: 45px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 10px;
font-size: 1.5rem;
}
/* Stats Cards */
.stats-overview {
margin-bottom: 2rem;
}
.stat-card {
padding: 1.5rem;
border-radius: 1rem;
color: white;
display: flex;
align-items: center;
gap: 1.5rem;
height: 100%;
transition: transform 0.3s ease;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
position: relative;
overflow: hidden;
margin-bottom: 0;
}
.stat-card:hover {
transform: translateY(-5px);
}
.stat-icon {
font-size: 2rem;
opacity: 0.9;
}
.stat-details {
flex-grow: 1;
}
.stat-value {
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 0.25rem;
line-height: 1.2;
}
.stat-label {
margin: 0;
opacity: 0.9;
font-size: 0.875rem;
}
/* Recommendation Card */
.recommendation-card {
background: #fff;
border-radius: 1rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
overflow: hidden;
margin-bottom: 2rem;
}
.recommendation-card:last-child {
margin-bottom: 0;
}
.card-header {
padding: 1.25rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
.card-body {
padding: 1.5rem;
}
/* Criteria Cards */
.criteria-card {
background: #fff;
border-radius: 0.75rem;
padding: 1.25rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
transition: transform 0.3s ease;
border: 1px solid rgba(0, 0, 0, 0.05);
height: 100%;
margin-bottom: 0;
}
.criteria-card:hover {
transform: translateY(-3px);
}
.criteria-icon {
color: #4caf50;
font-size: 1.25rem;
margin-right: 1rem;
flex-shrink: 0;
}
.criteria-details {
flex-grow: 1;
min-width: 0;
}
.criteria-name {
font-weight: 600;
color: #343a40;
margin-bottom: 0.25rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.criteria-weight {
font-size: 0.875rem;
color: #6c757d;
}
/* Table Section */
.table-section {
background: #fff;
border-radius: 1rem;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
margin-top: 1.5rem;
}
.table-header {
padding: 1rem 1.25rem;
color: white;
}
.table-content {
padding: 1.25rem;
}
.table {
margin-bottom: 0;
}
.table th {
background: #f8f9fa;
font-weight: 600;
padding: 1rem;
white-space: nowrap;
}
.table td {
padding: 1rem;
vertical-align: middle;
}
.score-cell {
font-family: 'Roboto Mono', monospace;
color: #2196f3;
}
/* Alerts */
.admin-alert {
border-radius: 0.75rem;
padding: 1rem 1.25rem;
margin-bottom: 1.5rem;
border: none;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
/* Gradients */
.bg-gradient-primary { background: linear-gradient(135deg, #2196f3, #1976d2); }
.bg-gradient-success { background: linear-gradient(135deg, #4caf50, #388e3c); }
.bg-gradient-info { background: linear-gradient(135deg, #00bcd4, #0097a7); }
.bg-gradient-orange { background: linear-gradient(135deg, #ff8a00, #ff5f00); }
/* Consistency Cards */
.consistency-section {
margin-bottom: 2rem;
}
.consistency-card {
background: #fff;
border-radius: 1rem;
padding: 1.25rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
border: 1px solid rgba(0, 0, 0, 0.05);
}
.consistency-card.consistent {
border-left: 4px solid #4caf50;
}
.consistency-card.inconsistent {
border-left: 4px solid #ff9800;
}
.consistency-card:hover {
transform: translateY(-3px);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.consistency-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
.consistency-header h6 {
font-weight: 600;
color: #2c3e50;
margin: 0;
}
.consistency-icon {
font-size: 1.25rem;
}
.consistency-card.consistent .consistency-icon {
color: #4caf50;
}
.consistency-card.inconsistent .consistency-icon {
color: #ff9800;
}
.consistency-details {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.detail-item {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.9rem;
}
.detail-item .label {
color: #6c757d;
font-weight: 500;
}
.detail-item .value {
font-family: 'Roboto Mono', monospace;
font-weight: 500;
color: #2196f3;
}
.status-badge {
margin-top: 0.5rem;
padding: 0.5rem;
border-radius: 0.5rem;
font-size: 0.85rem;
text-align: center;
font-weight: 500;
}
.status-badge.consistent {
background: rgba(76, 175, 80, 0.1);
color: #2e7d32;
}
.status-badge.inconsistent {
background: rgba(255, 152, 0, 0.1);
color: #ef6c00;
}
/* Responsive Adjustments */
@media (max-width: 1200px) {
.stat-value {
font-size: 1.25rem;
}
.stat-icon {
font-size: 1.75rem;
}
}
@media (max-width: 768px) {
.admin-container {
padding: 1rem;
}
.page-header {
padding: 1rem;
}
.stat-card {
padding: 1.25rem;
}
.card-body {
padding: 1.25rem;
}
.table-responsive {
margin: 0 -1.25rem;
}
.consistency-card {
padding: 1rem;
}
.consistency-header {
margin-bottom: 0.75rem;
padding-bottom: 0.5rem;
}
.detail-item {
font-size: 0.85rem;
}
.status-badge {
font-size: 0.8rem;
padding: 0.4rem;
}
}
@media (max-width: 576px) {
.stat-card {
padding: 1rem;
}
.stat-icon {
font-size: 1.5rem;
}
.criteria-card {
padding: 1rem;
}
}
</style>
@endpush
@endsection
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('.toggle-btn').forEach(button => {
button.addEventListener('click', function () {
const target = document.querySelector(this.dataset.target);
target.classList.toggle('d-none');
this.textContent = target.classList.contains('d-none')
? '🔁 Lihat Alternatif'
: '🔼 Sembunyikan Alternatif';
});
});
});
</script>
@endpush

View File

@ -0,0 +1,307 @@
@extends('layout.app')
@section('title', 'Hasil Rekomendasi')
@push('styles')
<style>
.recommendation-card {
border: none;
border-radius: 15px;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
margin-bottom: 2rem;
transition: all 0.3s ease;
}
.recommendation-card:hover {
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
}
.recommendation-header {
background: linear-gradient(45deg, #0d6efd, #0dcaf0);
color: white;
padding: 1.5rem;
border-radius: 15px 15px 0 0;
}
.recommendation-body {
padding: 1.5rem;
}
.component-section {
margin-bottom: 2rem;
border-radius: 15px;
overflow: hidden;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
}
.component-header {
background: linear-gradient(45deg, #0d6efd, #0dcaf0);
color: white;
padding: 1rem 1.5rem;
border-radius: 15px 15px 0 0;
}
.component-body {
background: white;
padding: 1.5rem;
border-radius: 0 0 15px 15px;
}
.nutrition-badge {
display: inline-block;
padding: 0.35rem 0.65rem;
font-size: 0.875rem;
font-weight: 500;
line-height: 1;
color: #fff;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
border-radius: 0.25rem;
background: linear-gradient(45deg, #0d6efd, #0dcaf0);
margin: 0.25rem;
}
.score-cell {
font-weight: 500;
color: #0d6efd;
}
.highest-score {
background-color: #d1e7dd !important;
color: #0f5132;
font-weight: bold;
}
.criteria-weight {
font-size: 0.875rem;
color: #6c757d;
}
.consistency-info {
background: #e9ecef;
padding: 1rem;
border-radius: 10px;
margin-bottom: 1.5rem;
}
.consistency-info p {
margin-bottom: 0.5rem;
color: #495057;
}
.consistency-info i {
margin-right: 0.5rem;
color: #0d6efd;
}
.ahp-steps {
counter-reset: step;
padding-left: 0;
list-style: none;
}
.ahp-steps li {
position: relative;
padding: 1rem 1rem 1rem 3rem;
background: #f8f9fa;
border-radius: 10px;
margin-bottom: 1rem;
}
.ahp-steps li::before {
counter-increment: step;
content: counter(step);
position: absolute;
left: 1rem;
top: 50%;
transform: translateY(-50%);
width: 1.5rem;
height: 1.5rem;
background: #0d6efd;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.875rem;
font-weight: bold;
}
</style>
@endpush
@section('content')
<div class="container-fluid">
<div class="row">
<div class="col-12">
<!-- Page Title -->
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h3 class="mb-0">Hasil Rekomendasi Makanan</h3>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{{ route('alternatif.pilih') }}">Pilih Alternatif</a></li>
<li class="breadcrumb-item"><a href="{{ route('alternatif.perbandingan') }}">Perbandingan</a></li>
<li class="breadcrumb-item active">Hasil Rekomendasi</li>
</ol>
</nav>
</div>
</div>
@if(session('error'))
<div class="alert alert-danger">
{{ session('error') }}
</div>
@endif
@if($makanans->isEmpty())
<div class="alert alert-warning">
<h5><i class="icon fas fa-exclamation-triangle"></i> Tidak ada data!</h5>
<p>Tidak ada makanan yang dipilih atau data tidak lengkap. Silakan kembali ke halaman pemilihan makanan.</p>
<a href="{{ route('alternatif.pilih') }}" class="btn btn-warning mt-3">
<i class="fas fa-arrow-left mr-2"></i> Kembali ke Pemilihan Makanan
</a>
</div>
@else
<!-- AHP Process Steps -->
<div class="recommendation-card mb-4">
<div class="recommendation-header">
<h5 class="mb-0">
<i class="fas fa-list-ol me-2"></i>Proses Perhitungan AHP
@if($waktuMakan)
- {{ $waktuMakan->nama }}
@endif
</h5>
</div>
<div class="recommendation-body">
<ol class="ahp-steps">
<li>Perhitungan bobot kriteria berdasarkan perbandingan berpasangan</li>
<li>Normalisasi matriks perbandingan kriteria</li>
<li>Perhitungan nilai alternatif untuk setiap kriteria</li>
<li>Normalisasi nilai alternatif</li>
<li>Perhitungan skor akhir dengan mengalikan bobot kriteria</li>
<li>Perangkingan alternatif berdasarkan skor akhir</li>
</ol>
</div>
</div>
<!-- Consistency Information -->
<div class="consistency-info">
<h5 class="mb-3"><i class="fas fa-check-circle"></i>Informasi Konsistensi</h5>
<p><i class="fas fa-calculator"></i>Consistency Ratio (CR): {{ number_format($consistencyRatio, 3) }}</p>
<p><i class="fas fa-info-circle"></i>Status:
@if($consistencyRatio < 0.1)
<span class="text-success">Konsisten (CR < 0.1)</span>
@else
<span class="text-danger">Tidak Konsisten (CR > 0.1)</span>
@endif
</p>
</div>
<!-- Results by Component -->
@foreach($komponens as $komponen)
@php
$komponenMakanans = $makanans->where('komponen_id', $komponen->id);
@endphp
@if($komponenMakanans->isNotEmpty())
<div class="component-section" data-aos="fade-up">
<div class="component-header">
<h5 class="mb-0">
<i class="fas fa-utensils me-2"></i>{{ $komponen->nama }}
</h5>
</div>
<div class="component-body">
<!-- Criteria Weights -->
<div class="mb-4">
<h6 class="mb-3">Bobot Kriteria:</h6>
<div class="row">
@foreach($kriterias as $kriteria)
<div class="col-md-3 mb-3">
<div class="p-3 bg-light rounded">
<strong>{{ $kriteria->nama }}</strong>
<div class="criteria-weight">
Bobot: {{ number_format($bobotKriteria[$kriteria->id] * 100, 1) }}%
</div>
</div>
</div>
@endforeach
</div>
</div>
<!-- Alternative Rankings -->
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>Ranking</th>
<th>Alternatif Makanan</th>
@foreach($kriterias as $kriteria)
<th>{{ $kriteria->nama }}</th>
@endforeach
<th>Total Skor</th>
</tr>
</thead>
<tbody>
@php
$sortedMakanans = $komponenMakanans->sortByDesc(function($makanan) use ($totalSkorMakanan) {
return $totalSkorMakanan[$makanan->id] ?? 0;
});
@endphp
@foreach($sortedMakanans as $index => $makanan)
<tr>
<td>{{ $index + 1 }}</td>
<td>
<strong>{{ $makanan->nama }}</strong>
<div class="mt-2">
<span class="nutrition-badge">
<i class="fas fa-fire me-1"></i>{{ $makanan->energi }} kal
</span>
<span class="nutrition-badge">
<i class="fas fa-hamburger me-1"></i>{{ $makanan->lemak }}g
</span>
<span class="nutrition-badge">
<i class="fas fa-bread-slice me-1"></i>{{ $makanan->karbohidrat }}g
</span>
<span class="nutrition-badge">
<i class="fas fa-salt-shaker me-1"></i>{{ $makanan->natrium }}mg
</span>
</div>
</td>
@foreach($kriterias as $kriteria)
<td class="score-cell">
{{ number_format($bobotMakanan[$makanan->id][$kriteria->id], 3) }}
</td>
@endforeach
<td class="score-cell {{ $index === 0 ? 'highest-score' : '' }}">
{{ number_format($totalSkorMakanan[$makanan->id], 3) }}
@if($index === 0)
<span class="badge bg-success ms-1">Terbaik</span>
@endif
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
@endif
@endforeach
<!-- Next Step Button -->
<div class="d-flex justify-content-between mt-4 mb-5">
<a href="{{ route('alternatif.perbandingan') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left me-2"></i>Kembali ke Perbandingan
</a>
<a href="{{ route('admindash') }}" class="btn btn-primary">
<i class="fas fa-home me-2"></i>Kembali ke Dashboard
</a>
</div>
@endif
</div>
</div>
</div>
@endsection

View File

@ -0,0 +1,167 @@
@extends('layout.app')
@section('title', 'Tambah Relasi')
@include('admin.shared.admin-styles')
@section('content')
<div class="admin-container container-fluid">
<!-- Page Header -->
<div class="page-header animate-fade-in">
<div class="row align-items-center">
<div class="col-12">
<h3 class="mb-2 text-white">
<i class="fas fa-plus-circle me-2"></i>Tambah Relasi
</h3>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}" class="text-white-50">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{{ route('relasi.index') }}" class="text-white-50">Relasi</a></li>
<li class="breadcrumb-item active text-white" aria-current="page">Tambah</li>
</ol>
</nav>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="admin-card animate-fade-in">
<div class="card-body">
<h5 class="card-title mb-4">
<i class="fas fa-plus me-2"></i>Tambah Relasi Baru
</h5>
@if(session('success'))
<div class="alert alert-success" role="alert">
<i class="fas fa-check-circle me-2"></i>{{ session('success') }}
</div>
@endif
@if(session('error'))
<div class="alert alert-danger" role="alert">
<i class="fas fa-exclamation-circle me-2"></i>{{ session('error') }}
</div>
@endif
<form action="{{ route('relasi.store') }}" method="POST">
@csrf
<div class="row g-4">
<div class="col-md-4">
<div class="admin-form-group">
<label class="admin-form-label">
<i class="fas fa-utensils me-2"></i>Makanan
</label>
<select name="makanan_id" class="admin-form-control @error('makanan_id') is-invalid @enderror">
<option value="">Pilih Makanan</option>
@foreach($makanans as $makanan)
<option value="{{ $makanan->id }}" {{ old('makanan_id') == $makanan->id ? 'selected' : '' }}>
{{ $makanan->nama }}
</option>
@endforeach
</select>
@error('makanan_id')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div class="col-md-4">
<div class="admin-form-group">
<label class="admin-form-label">
<i class="fas fa-cube me-2"></i>Komponen
</label>
<select name="komponen_id" class="admin-form-control @error('komponen_id') is-invalid @enderror">
<option value="">Pilih Komponen</option>
@foreach($komponens as $komponen)
<option value="{{ $komponen->id }}" {{ old('komponen_id') == $komponen->id ? 'selected' : '' }}>
{{ $komponen->nama }}
</option>
@endforeach
</select>
@error('komponen_id')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div class="col-md-4">
<div class="admin-form-group">
<label class="admin-form-label">
<i class="fas fa-clock me-2"></i>Waktu Makan
</label>
<select name="waktu_makan_id" class="admin-form-control @error('waktu_makan_id') is-invalid @enderror">
<option value="">Pilih Waktu Makan</option>
@foreach($waktuMakans as $waktu)
<option value="{{ $waktu->id }}" {{ old('waktu_makan_id') == $waktu->id ? 'selected' : '' }}>
{{ $waktu->nama }}
</option>
@endforeach
</select>
@error('waktu_makan_id')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div class="col-md-4">
<div class="admin-form-group">
<label class="admin-form-label">
<i class="fas fa-toggle-on me-2"></i>Status
</label>
<select name="status" class="admin-form-control @error('status') is-invalid @enderror">
<option value="1" {{ old('status', '1') == '1' ? 'selected' : '' }}>Aktif</option>
<option value="0" {{ old('status') == '0' ? 'selected' : '' }}>Tidak Aktif</option>
</select>
@error('status')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
</div>
<div class="text-end mt-4">
<a href="{{ route('relasi.index') }}" class="admin-btn btn-secondary me-2">
<i class="fas fa-arrow-left me-2"></i>Kembali
</a>
<button type="submit" class="admin-btn btn-primary">
<i class="fas fa-save me-2"></i>Simpan
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
// Initialize Select2
$('.admin-form-control').select2({
theme: 'bootstrap-5',
width: '100%'
});
// Animation
const animateElements = document.querySelectorAll('.animate-fade-in');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.opacity = 1;
entry.target.style.transform = 'translateY(0)';
}
});
});
animateElements.forEach(element => {
element.style.opacity = 0;
element.style.transform = 'translateY(20px)';
observer.observe(element);
});
});
</script>
@endpush

View File

@ -1,52 +1,168 @@
@extends('layout.app')
@section('title', 'Edit Relasi')
@include('admin.shared.admin-styles')
@section('content')
<div class="container">
<h2 class="mb-4">Edit Relasi</h2>
@if(session('error'))
<div class="alert alert-danger">{{ session('error') }}</div>
@endif
<form method="POST" action="{{ route('relasi.update', $relasi->id) }}">
@csrf
@method('PUT')
<div class="form-group mb-3">
<label for="makanan_id">Makanan</label>
<select name="makanan_id" class="form-control">
@foreach($makanans as $makanan)
<option value="{{ $makanan->id }}" {{ $relasi->makanan_id == $makanan->id ? 'selected' : '' }}>
{{ $makanan->nama }}
</option>
@endforeach
</select>
<div class="admin-container container-fluid">
<!-- Page Header -->
<div class="page-header animate-fade-in">
<div class="row align-items-center">
<div class="col-12">
<h3 class="mb-2 text-white">
<i class="fas fa-edit me-2"></i>Edit Relasi
</h3>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}" class="text-white-50">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{{ route('relasi.index') }}" class="text-white-50">Relasi</a></li>
<li class="breadcrumb-item active text-white" aria-current="page">Edit</li>
</ol>
</nav>
</div>
</div>
</div>
<div class="form-group mb-3">
<label for="komponen_id">Komponen</label>
<select name="komponen_id" class="form-control">
@foreach($komponens as $komponen)
<option value="{{ $komponen->id }}" {{ $relasi->komponen_id == $komponen->id ? 'selected' : '' }}>
{{ $komponen->nama }}
</option>
@endforeach
</select>
<div class="row">
<div class="col-12">
<div class="admin-card animate-fade-in">
<div class="card-body">
<h5 class="card-title mb-4">
<i class="fas fa-edit me-2"></i>Edit Relasi
</h5>
@if(session('success'))
<div class="alert alert-success" role="alert">
<i class="fas fa-check-circle me-2"></i>{{ session('success') }}
</div>
@endif
@if(session('error'))
<div class="alert alert-danger" role="alert">
<i class="fas fa-exclamation-circle me-2"></i>{{ session('error') }}
</div>
@endif
<form action="{{ route('relasi.update', $relasi->id) }}" method="POST">
@csrf
@method('PUT')
<div class="row g-4">
<div class="col-md-4">
<div class="admin-form-group">
<label class="admin-form-label">
<i class="fas fa-utensils me-2"></i>Makanan
</label>
<select name="makanan_id" class="admin-form-control @error('makanan_id') is-invalid @enderror">
<option value="">Pilih Makanan</option>
@foreach($makanans as $makanan)
<option value="{{ $makanan->id }}" {{ old('makanan_id', $relasi->makanan_id) == $makanan->id ? 'selected' : '' }}>
{{ $makanan->nama }}
</option>
@endforeach
</select>
@error('makanan_id')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div class="col-md-4">
<div class="admin-form-group">
<label class="admin-form-label">
<i class="fas fa-cube me-2"></i>Komponen
</label>
<select name="komponen_id" class="admin-form-control @error('komponen_id') is-invalid @enderror">
<option value="">Pilih Komponen</option>
@foreach($komponens as $komponen)
<option value="{{ $komponen->id }}" {{ old('komponen_id', $relasi->komponen_id) == $komponen->id ? 'selected' : '' }}>
{{ $komponen->nama }}
</option>
@endforeach
</select>
@error('komponen_id')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div class="col-md-4">
<div class="admin-form-group">
<label class="admin-form-label">
<i class="fas fa-clock me-2"></i>Waktu Makan
</label>
<select name="waktu_makan_id" class="admin-form-control @error('waktu_makan_id') is-invalid @enderror">
<option value="">Pilih Waktu Makan</option>
@foreach($waktuMakans as $waktu)
<option value="{{ $waktu->id }}" {{ old('waktu_makan_id', $relasi->waktu_makan_id) == $waktu->id ? 'selected' : '' }}>
{{ $waktu->nama }}
</option>
@endforeach
</select>
@error('waktu_makan_id')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div class="col-md-4">
<div class="admin-form-group">
<label class="admin-form-label">
<i class="fas fa-toggle-on me-2"></i>Status
</label>
<select name="status" class="admin-form-control @error('status') is-invalid @enderror">
<option value="1" {{ old('status', $relasi->status) == '1' ? 'selected' : '' }}>Aktif</option>
<option value="0" {{ old('status', $relasi->status) == '0' ? 'selected' : '' }}>Tidak Aktif</option>
</select>
@error('status')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
</div>
<div class="text-end mt-4">
<a href="{{ route('relasi.index') }}" class="admin-btn btn-secondary me-2">
<i class="fas fa-arrow-left me-2"></i>Kembali
</a>
<button type="submit" class="admin-btn btn-primary">
<i class="fas fa-save me-2"></i>Simpan Perubahan
</button>
</div>
</form>
</div>
</div>
</div>
<div class="form-group mb-3">
<label for="waktu_makan_id">Waktu Makan</label>
<select name="waktu_makan_id" class="form-control">
@foreach($waktuMakans as $waktu)
<option value="{{ $waktu->id }}" {{ $relasi->waktu_makan_id == $waktu->id ? 'selected' : '' }}>
{{ $waktu->nama }}
</option>
@endforeach
</select>
</div>
<button type="submit" class="btn btn-success">Simpan Perubahan</button>
<a href="{{ route('relasi') }}" class="btn btn-secondary">Batal</a>
</form>
</div>
</div>
@endsection
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
// Initialize Select2
$('.admin-form-control').select2({
theme: 'bootstrap-5',
width: '100%'
});
// Animation
const animateElements = document.querySelectorAll('.animate-fade-in');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.opacity = 1;
entry.target.style.transform = 'translateY(0)';
}
});
});
animateElements.forEach(element => {
element.style.opacity = 0;
element.style.transform = 'translateY(20px)';
observer.observe(element);
});
});
</script>
@endpush

View File

@ -1,143 +1,128 @@
@extends('layout.app')
@section('title', 'Relasi Makanan')
@include('admin.shared.admin-styles')
@section('content')
<div class="container">
<h2 class="mb-4">Tambah Relasi Makanan - Komponen - Waktu Makan</h2>
@if(session('success'))
<div class="alert alert-success">{{ session('success') }}</div>
@elseif(session('error'))
<div class="alert alert-danger">{{ session('error') }}</div>
@endif
<form method="POST" action="{{ route('relasi.store') }}" class="mb-4">
@csrf
<div class="form-group mb-2">
<label for="makanan_id">Makanan</label>
<select name="makanan_id" class="form-control">
@foreach($makanans as $makanan)
<option value="{{ $makanan->id }}">{{ $makanan->nama }}</option>
@endforeach
</select>
</div>
<div class="form-group mb-2">
<label for="komponen_id">Komponen</label>
<select name="komponen_id" class="form-control">
@foreach($komponens as $komponen)
<option value="{{ $komponen->id }}">{{ $komponen->nama }}</option>
@endforeach
</select>
</div>
<div class="form-group mb-3">
<label for="waktu_makan_id">Waktu Makan</label>
<select name="waktu_makan_id" class="form-control">
@foreach($waktuMakans as $waktu)
<option value="{{ $waktu->id }}">{{ $waktu->nama }}</option>
@endforeach
</select>
</div>
<button type="submit" class="btn btn-primary">Simpan Relasi</button>
</form>
<section class="section">
<div class="row">
<div class="col-lg-12">
<div class="card">
<div class="card-body">
<h5 class="card-title">Filter Relasi</h5>
<form method="GET" action="{{ route('relasi') }}" class="row g-3 align-items-end mb-4">
<div class="col-md-4">
<label for="komponen_id" class="form-label">Filter Komponen</label>
<select name="komponen_id" class="form-select">
<option value="">-- Semua Komponen --</option>
@foreach($komponens as $komponen)
<option value="{{ $komponen->id }}" {{ request('komponen_id') == $komponen->id ? 'selected' : '' }}>
{{ $komponen->nama }}
</option>
@endforeach
</select>
</div>
<div class="col-md-4">
<label for="waktu_makan_id" class="form-label">Filter Waktu Makan</label>
<select name="waktu_makan_id" class="form-select">
<option value="">-- Semua Waktu Makan --</option>
@foreach($waktuMakans as $waktu)
<option value="{{ $waktu->id }}" {{ request('waktu_makan_id') == $waktu->id ? 'selected' : '' }}>
{{ $waktu->nama }}
</option>
@endforeach
</select>
</div>
<div class="col-md-4">
<button type="submit" class="btn btn-primary">Terapkan Filter</button>
</div>
</form>
<hr>
<div class="d-flex justify-content-between align-items-center mb-2">
<h5 class="card-title mb-0">Data Relasi Tersimpan</h5>
</div>
<!-- Table with stripped rows -->
<table class="table datatable">
<thead>
<tr>
<th class="text-center">No</th>
<th class="text-center">Makanan</th>
<th class="text-center">Komponen</th>
<th class="text-center">Waktu Makanan</th>
<th class="text-center">Action</th>
</tr>
</thead>
<tbody>
@foreach ($relasis as $relasi)
<tr>
<td class="text-center">{{ $loop->iteration }}</td>
<td class="text-center">{{ $relasi->makanan->nama }}</td>
<td class="text-center">{{ $relasi->komponen->nama }}</td>
<td class="text-center">{{ $relasi->waktuMakan->nama }}</td>
<td class="text-center">
<a href="{{ route('relasi.edit', $relasi->id) }}" class="btn btn-warning btn-sm" title="Edit">
<i class="fas fa-edit"></i>
</a>
<form action="{{ route('relasi.destroy', $relasi->id) }}" method="POST" onsubmit="return confirm('Yakin ingin menghapus?');" style="display:inline;">
@csrf
@method('DELETE')
<button class="btn btn-danger btn-sm" title="Hapus">
<i class="fas fa-trash-alt"></i>
</button>
</form>
</td>
{{-- <td class="text-center">
<form action="{{ route('relasi.destroy', $relasi->id) }}" method="POST" onsubmit="return confirm('Yakin ingin menghapus?');" style="display:inline;">
@csrf
@method('DELETE')
<button class="btn btn-danger btn-sm" title="Hapus">
<i class="fas fa-trash-alt"></i>
</button>
</form>
</td> --}}
</tr>
@endforeach
</tbody>
</table>
<!-- End Table -->
</div>
<div class="admin-container container-fluid">
<!-- Page Header -->
<div class="page-header animate-fade-in">
<div class="row align-items-center">
<div class="col-12">
<h3 class="mb-2 text-white">
<i class="fas fa-link me-2"></i>Relasi Makanan
</h3>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}" class="text-white-50">Dashboard</a></li>
<li class="breadcrumb-item active text-white" aria-current="page">Relasi Makanan</li>
</ol>
</nav>
</div>
</div>
</div>
</section>
<div class="row">
<div class="col-12">
<div class="admin-card animate-fade-in">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-4">
<h5 class="card-title mb-0">
<i class="fas fa-list me-2"></i>Daftar Relasi
</h5>
<a href="{{ route('relasi.create') }}" class="admin-btn btn-primary">
<i class="fas fa-plus me-2"></i>Tambah Relasi
</a>
</div>
<div class="table-responsive">
<table class="admin-table">
<thead>
<tr>
<th>No</th>
<th>Makanan</th>
<th>Komponen</th>
<th>Waktu Makan</th>
<th>Status</th>
<th class="text-center">Aksi</th>
</tr>
</thead>
<tbody>
@forelse($relasis as $index => $relasi)
<tr>
<td>{{ $index + 1 }}</td>
<td>
<div class="d-flex align-items-center">
{{ $relasi->makanan->nama }}
</div>
</td>
<td>{{ $relasi->komponen->nama }}</td>
<td>{{ $relasi->waktuMakan->nama }}</td>
<td>
<span class="admin-badge {{ $relasi->status ? 'bg-success' : 'bg-danger' }}">
{{ $relasi->status ? 'Aktif' : 'Tidak Aktif' }}
</span>
</td>
<td class="text-center">
<a href="{{ route('relasi.edit', $relasi->id) }}"
class="admin-btn btn-warning btn-sm me-2">
<i class="fas fa-edit"></i>
</a>
<form action="{{ route('relasi.destroy', $relasi->id) }}"
method="POST"
class="d-inline delete-form"
onsubmit="return confirm('Apakah Anda yakin ingin menghapus data ini?');">
@csrf
@method('DELETE')
<button type="submit" class="admin-btn btn-danger btn-sm">
<i class="fas fa-trash"></i>
</button>
</form>
</td>
</tr>
@empty
<tr>
<td colspan="6" class="text-center py-5">
<i class="fas fa-folder-open fa-3x text-muted mb-3"></i>
<p class="text-muted">Tidak ada data relasi</p>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@if($relasis->hasPages())
<div class="d-flex justify-content-center mt-4">
{{ $relasis->links() }}
</div>
@endif
</div>
</div>
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
const animateElements = document.querySelectorAll('.animate-fade-in');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.opacity = 1;
entry.target.style.transform = 'translateY(0)';
}
});
});
animateElements.forEach(element => {
element.style.opacity = 0;
element.style.transform = 'translateY(20px)';
observer.observe(element);
});
});
</script>
@endpush

View File

@ -1,59 +1,78 @@
@extends('layout.app')
@section('title', 'Role Management')
@include('admin.shared.admin-styles')
@section('content')
<div class="pagetitle">
<div class="admin-container container-fluid">
<!-- Page Header -->
<div class="page-header animate-fade-in">
<div class="row align-items-center">
<div class="col-12">
<h3 class="mb-2 text-white">
<i class="fas fa-user-tag me-2"></i>Role Management
</h3>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}" class="text-white-50">Dashboard</a></li>
<li class="breadcrumb-item active text-white" aria-current="page">Roles</li>
</ol>
</nav>
</div>
</div>
</div>
<h1>Role</h1>
<nav>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ route('admindash') }}">Home</a></li>
<li class="breadcrumb-item active"><a href="{{ route('role') }}"> role</a></li>
</ol>
</nav>
</div><!-- End Page Title -->
<section class="section">
<div class="row">
<div class="col-lg-12">
<div class="card">
<div class="col-12">
<div class="admin-card animate-fade-in">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<h5 class="card-title mb-2">role</h5>
<div class="text-center">
<a href="{{ route('tambahrole') }}" class="btn btn-primary">Tambah</a>
</div>
<div class="d-flex justify-content-between align-items-center mb-4">
<h5 class="card-title mb-0">
<i class="fas fa-list me-2"></i>Role List
</h5>
<a href="{{ route('tambahrole') }}" class="admin-btn btn-primary">
<i class="fas fa-plus me-2"></i>Add New Role
</a>
</div>
<!-- Table with stripped rows -->
<table class="table datatable">
<thead>
<tr>
<th class="text-center">No</th>
<th class="text-center">Role</th>
<th class="text-center">Action</th>
</tr>
</thead>
<tbody>
@foreach ($roles as $rl)
<tr>
<td class="text-center">{{ $loop->iteration }}</td>
<td class="text-center">{{ $rl->name }}</td>
<td class="text-center">
<a href="{{ route('editrole', $rl->id) }}" class="btn btn-sm btn-warning">Edit</a>
<form action="{{ url('/role', $rl->id) }}" method="POST" style="display: inline;">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-sm btn-danger">Hapus</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
<!-- End Table with stripped rows -->
<div class="table-responsive">
<table class="admin-table">
<thead>
<tr>
<th class="text-center" width="10%">No</th>
<th>Role Name</th>
<th class="text-center" width="20%">Actions</th>
</tr>
</thead>
<tbody>
@foreach ($roles as $rl)
<tr>
<td class="text-center">{{ $loop->iteration }}</td>
<td>{{ $rl->name }}</td>
<td class="text-center">
<a href="{{ route('editrole', $rl->id) }}"
class="admin-btn btn-warning btn-sm me-2">
<i class="fas fa-edit"></i>
</a>
<form action="{{ url('/role', $rl->id) }}" method="POST"
class="d-inline"
onsubmit="return confirm('Are you sure you want to delete this role?');">
@csrf
@method('DELETE')
<button type="submit" class="admin-btn btn-danger btn-sm">
<i class="fas fa-trash"></i>
</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
@endsection

View File

@ -0,0 +1,269 @@
@push('styles')
<style>
/* Common Variables */
:root {
--primary-gradient: linear-gradient(135deg, #0d6efd 0%, #0099ff 100%);
--secondary-gradient: linear-gradient(135deg, #6c757d 0%, #868e96 100%);
--success-gradient: linear-gradient(135deg, #198754 0%, #28a745 100%);
--warning-gradient: linear-gradient(135deg, #ffc107 0%, #ffcd39 100%);
--danger-gradient: linear-gradient(135deg, #dc3545 0%, #e35d6a 100%);
--card-shadow: 0 2px 4px rgba(0,0,0,0.1);
--card-shadow-hover: 0 4px 8px rgba(0,0,0,0.2);
--transition-speed: 0.3s;
--border-radius: 12px;
--spacing-unit: 1rem;
}
/* Layout & Containers */
.admin-container {
padding: var(--spacing-unit) 0;
background-color: #f8f9fa;
min-height: calc(100vh - 60px);
}
.page-header {
background: var(--primary-gradient);
padding: calc(var(--spacing-unit) * 2);
border-radius: var(--border-radius);
margin-bottom: calc(var(--spacing-unit) * 2);
color: white;
box-shadow: var(--card-shadow);
}
.stats-container {
background: white;
border-radius: var(--border-radius);
padding: var(--spacing-unit);
margin-bottom: var(--spacing-unit);
box-shadow: var(--card-shadow);
}
/* Cards */
.admin-card {
transition: all var(--transition-speed) ease;
border: none;
box-shadow: var(--card-shadow);
border-radius: var(--border-radius);
height: 100%;
background: white;
overflow: hidden;
}
.admin-card:hover {
transform: translateY(-5px);
box-shadow: var(--card-shadow-hover);
}
.admin-card .card-header {
border-radius: var(--border-radius) var(--border-radius) 0 0 !important;
padding: var(--spacing-unit);
border: none;
background: white;
}
.admin-card .card-body {
padding: var(--spacing-unit);
background: white;
}
.admin-card .card-footer {
background: white;
border-top: 1px solid rgba(0,0,0,0.1);
padding: var(--spacing-unit);
}
/* Feature Cards */
.feature-card {
position: relative;
overflow: hidden;
border-radius: var(--border-radius);
background: white;
transition: all var(--transition-speed) ease;
}
.feature-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 4px;
background: var(--primary-gradient);
}
.feature-card:hover {
transform: translateY(-5px);
box-shadow: var(--card-shadow-hover);
}
.feature-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
margin-bottom: 1rem;
background: var(--primary-gradient);
color: white;
}
/* Badges & Labels */
.admin-badge {
font-weight: 500;
padding: 0.5em 0.8em;
border-radius: 6px;
transition: all 0.2s ease;
background: var(--primary-gradient);
color: white;
}
.admin-badge:hover {
transform: scale(1.05);
}
/* Buttons */
.admin-btn {
padding: 0.5rem 1rem;
border-radius: 6px;
transition: all var(--transition-speed) ease;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
font-weight: 500;
color: white;
border: none;
}
.admin-btn.btn-primary {
background: var(--primary-gradient);
}
.admin-btn.btn-secondary {
background: var(--secondary-gradient);
}
.admin-btn.btn-success {
background: var(--success-gradient);
}
.admin-btn.btn-warning {
background: var(--warning-gradient);
}
.admin-btn.btn-danger {
background: var(--danger-gradient);
}
.admin-btn:hover {
transform: translateY(-2px);
color: white;
}
.admin-btn i {
font-size: 0.9em;
}
/* Tables */
.admin-table {
width: 100%;
margin-bottom: 1rem;
background-color: white;
border-collapse: collapse;
border-radius: var(--border-radius);
overflow: hidden;
}
.admin-table th,
.admin-table td {
padding: 1rem;
vertical-align: middle;
border-top: 1px solid #dee2e6;
}
.admin-table thead th {
vertical-align: bottom;
border-bottom: 2px solid #dee2e6;
background-color: #f8f9fa;
font-weight: 600;
color: #495057;
}
.admin-table tbody tr:hover {
background-color: rgba(13, 110, 253, 0.05);
}
/* Forms */
.admin-form-group {
margin-bottom: 1rem;
}
.admin-form-label {
font-weight: 500;
margin-bottom: 0.5rem;
color: #495057;
}
.admin-form-control {
display: block;
width: 100%;
padding: 0.5rem 0.75rem;
font-size: 1rem;
line-height: 1.5;
border: 1px solid #ced4da;
border-radius: var(--border-radius);
transition: border-color var(--transition-speed) ease-in-out;
background: white;
}
.admin-form-control:focus {
border-color: #0d6efd;
outline: 0;
box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25);
}
/* Alerts */
.admin-alert {
border-radius: var(--border-radius);
margin-bottom: calc(var(--spacing-unit) * 2);
padding: 1rem;
border: none;
background: white;
box-shadow: var(--card-shadow);
}
/* Animations */
.animate-fade-in {
animation: fadeIn 0.5s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
/* Responsive Utilities */
@media (max-width: 768px) {
.page-header {
padding: var(--spacing-unit);
}
.stats-container {
padding: calc(var(--spacing-unit) * 0.5);
}
.admin-card {
margin-bottom: var(--spacing-unit);
}
.admin-table {
display: block;
width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
}
</style>
@endpush

Some files were not shown because too many files have changed in this diff Show More