diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8f0de65 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 + +[docker-compose.yml] +indent_size = 4 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ea0665b --- /dev/null +++ b/.env.example @@ -0,0 +1,59 @@ +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_APP_NAME="${APP_NAME}" +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}" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7fe978f --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +/.phpunit.cache +/node_modules +/public/build +/public/hot +/public/storage +/storage/*.key +/vendor +.env +.env.backup +.env.production +.phpunit.result.cache +Homestead.json +Homestead.yaml +auth.json +npm-debug.log +yarn-error.log +/.fleet +/.idea +/.vscode diff --git a/README.md b/README.md new file mode 100644 index 0000000..1a4c26b --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ +
+ + + +## About Laravel + +Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as: + +- [Simple, fast routing engine](https://laravel.com/docs/routing). +- [Powerful dependency injection container](https://laravel.com/docs/container). +- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage. +- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent). +- Database agnostic [schema migrations](https://laravel.com/docs/migrations). +- [Robust background job processing](https://laravel.com/docs/queues). +- [Real-time event broadcasting](https://laravel.com/docs/broadcasting). + +Laravel is accessible, powerful, and provides tools required for large, robust applications. + +## Learning Laravel + +Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework. + +You may also try the [Laravel Bootcamp](https://bootcamp.laravel.com), where you will be guided through building a modern Laravel application from scratch. + +If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library. + +## Laravel Sponsors + +We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com). + +### Premium Partners + +- **[Vehikl](https://vehikl.com/)** +- **[Tighten Co.](https://tighten.co)** +- **[WebReinvent](https://webreinvent.com/)** +- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)** +- **[64 Robots](https://64robots.com)** +- **[Curotec](https://www.curotec.com/services/technologies/laravel/)** +- **[Cyber-Duck](https://cyber-duck.co.uk)** +- **[DevSquad](https://devsquad.com/hire-laravel-developers)** +- **[Jump24](https://jump24.co.uk)** +- **[Redberry](https://redberry.international/laravel/)** +- **[Active Logic](https://activelogic.com)** +- **[byte5](https://byte5.de)** +- **[OP.GG](https://op.gg)** + +## Contributing + +Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions). + +## Code of Conduct + +In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct). + +## Security Vulnerabilities + +If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed. + +## License + +The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT). diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php new file mode 100644 index 0000000..e6b9960 --- /dev/null +++ b/app/Console/Kernel.php @@ -0,0 +1,27 @@ +command('inspire')->hourly(); + } + + /** + * Register the commands for the application. + */ + protected function commands(): void + { + $this->load(__DIR__.'/Commands'); + + require base_path('routes/console.php'); + } +} diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php new file mode 100644 index 0000000..56af264 --- /dev/null +++ b/app/Exceptions/Handler.php @@ -0,0 +1,30 @@ + + */ + protected $dontFlash = [ + 'current_password', + 'password', + 'password_confirmation', + ]; + + /** + * Register the exception handling callbacks for the application. + */ + public function register(): void + { + $this->reportable(function (Throwable $e) { + // + }); + } +} diff --git a/app/Http/Controllers/Admin/AlternatifController.php b/app/Http/Controllers/Admin/AlternatifController.php new file mode 100644 index 0000000..be0432f --- /dev/null +++ b/app/Http/Controllers/Admin/AlternatifController.php @@ -0,0 +1,154 @@ +get(); + + // Ambil semua kriteria + $kriteria = Kriteria::all(); + + // Ambil semua alternatif dengan relasi ke subkriteria + $query = DataAlternatif::with('subkriteria', 'penilaian'); + + // Jika ada filter jenis pakaian, gunakan filter berdasarkan subkriteria_id + if ($request->filled('jenis_pakaian')) { + $query->where('subkriteria_id', $request->jenis_pakaian); + } + + // Search + if ($request->filled('search')) { + $query->where('nama_alternatif', 'like', '%' . $request->search . '%'); + } + + // Ambil data alternatif dengan pagination (10 per halaman) + $perPage = $request->input('entries', 10); // Ambil dari URL ?entries=xx, default 10 + $alternatif = $query->paginate($perPage)->appends($request->query()); + + // Kirim data ke view + return view('admin.pages.alternatif.index', compact('subkriteria', 'alternatif', 'kriteria')); + } + + + public function store(Request $request) + { + // Validasi input + $request->validate([ + 'nama_alternatif' => 'required|string|max:255|unique:dataalternatif,nama_alternatif', + 'subkriteria_id' => 'required|exists:subkriteria,id', + 'gambar' => 'nullable|image|mimes:jpeg,png,jpg,gif|max:2048', + ]); + + $gambarPath = null; + + // Jika ada gambar yang diunggah, simpan ke folder public/assets/img + if ($request->hasFile('gambar')) { + $gambar = $request->file('gambar'); + $namaFile = time() . '_' . $gambar->getClientOriginalName(); + // simpan ke public/assets/img + $gambar->move(public_path('assets/img'), $namaFile); + $gambarPath = 'assets/img/' . $namaFile; + } + + // Simpan data alternatif ke database + $alternatif = DataAlternatif::create([ + 'nama_alternatif' => $request->nama_alternatif, + 'subkriteria_id' => $request->subkriteria_id, + 'gambar' => $gambarPath, + ]); + + // Ambil semua kriteria yang ada + $kriteria = Kriteria::all(); + + // Simpan nilai awal 0 untuk semua kriteria pada alternatif baru + foreach ($kriteria as $k) { + \App\Models\Penilaian::create([ + 'alternatif_id' => $alternatif->id, + 'kriteria_id' => $k->id, + 'nilai' => 0, // Atur nilai default + ]); + } + + // Redirect kembali ke halaman index dengan pesan sukses + return redirect()->route('admin.alternatif.index')->with('success', 'Data Alternatif berhasil ditambahkan.'); + } + + // Menampilkan form edit untuk alternatif + public function edit($id) + { + // Ambil data alternatif berdasarkan ID + $alternatif = DataAlternatif::findOrFail($id); + + // Ambil semua subkriteria (untuk pilihan dropdown misalnya) + $subkriteria = \App\Models\Subkriteria::all(); + + // Tampilkan view edit + return view('admin.pages.alternatif.edit', compact('alternatif', 'subkriteria')); + } + + public function update(Request $request, $id) + { + // Tampilkan view edit + $request->validate([ + 'nama_alternatif' => 'required|string|max:255|unique:dataalternatif,nama_alternatif,' . $id, + 'subkriteria_id' => 'required|exists:subkriteria,id', + 'gambar' => 'nullable|image|mimes:jpg,jpeg,png|max:2048', + ]); + + // Ambil data alternatif berdasarkan ID + $alternatif = DataAlternatif::findOrFail($id); + + // Update data dari input + $alternatif->nama_alternatif = $request->nama_alternatif; + $alternatif->subkriteria_id = $request->subkriteria_id; + + // Jika user mengunggah gambar baru, simpan dan update path-nya + if ($request->hasFile('gambar')) { + $file = $request->file('gambar'); + $namaFile = time() . '_' . $file->getClientOriginalName(); + + // Simpan di public/assets/img + $file->move(public_path('assets/img'), $namaFile); + + // Update path di DB + $alternatif->gambar = 'assets/img/' . $namaFile; + } + + // Simpan perubahan ke database + $alternatif->save(); + + // Jika permintaan AJAX, kirim response JSON + if ($request->ajax()) { + return response()->json(['success' => true, 'message' => 'Data Alternatif berhasil diperbarui!']); + } + + // Redirect ke halaman index dengan pesan sukses + return redirect()->route('admin.alternatif.index')->with('success', 'Data Alternatif berhasil diperbarui.'); + } + + public function destroy(Request $request, $id) + { + // Cari data berdasarkan ID dan hapus + $alternatif = DataAlternatif::findOrFail($id); + $alternatif->delete(); + + // Jika permintaan AJAX, kirim response JSON + if ($request->ajax()) { + return response()->json(['success' => true, 'message' => 'Data berhasil dihapus.']); + } + + // Redirect ke halaman index dengan pesan sukses + return redirect()->route('admin.alternatif.index')->with('success', 'Data Alternatif berhasil dihapus.'); + } +} diff --git a/app/Http/Controllers/Admin/AuthController.php b/app/Http/Controllers/Admin/AuthController.php new file mode 100644 index 0000000..e6ec0a1 --- /dev/null +++ b/app/Http/Controllers/Admin/AuthController.php @@ -0,0 +1,30 @@ +only('username', 'password'); + + if (Auth::attempt($credentials)) { + return redirect()->route('dashboard'); + } + + return back()->withErrors(['login' => 'Username atau password salah.']); + } +} diff --git a/app/Http/Controllers/Admin/DashboardController.php b/app/Http/Controllers/Admin/DashboardController.php new file mode 100644 index 0000000..60db684 --- /dev/null +++ b/app/Http/Controllers/Admin/DashboardController.php @@ -0,0 +1,36 @@ + $totalKriteria, + 'totalSubKriteria' => $totalSubKriteria, + 'totalAlternatif' => $totalAlternatif, + 'totalPenilaian' => $totalPenilaian, + 'totalPerhitungan' => $totalPenilaian, + 'totalHasilAkhir' => $totalPenilaian, + 'kriteriaId' => $kriteriaPertama ? $kriteriaPertama->id : null, + ]); + } +} diff --git a/app/Http/Controllers/Admin/DataKriteriaController.php b/app/Http/Controllers/Admin/DataKriteriaController.php new file mode 100644 index 0000000..23c689f --- /dev/null +++ b/app/Http/Controllers/Admin/DataKriteriaController.php @@ -0,0 +1,84 @@ +get(); + return view('admin.pages.kriteria.index', compact('kriteria')); + } + + // Menampilkan form tambah data + public function create() + { + return view('admin.pages.kriteria.create'); + } + + // Menyimpan data baru + public function store(Request $request) + { + // Validasi inputan dari form + $request->validate([ + 'kode_kriteria' => 'required|unique:datakriteria,kode_kriteria|max:10', + 'nama_kriteria' => 'required|max:255', + 'bobot' => 'required|numeric|min:0.01|max:1', // nilai > 0 + 'jenis' => 'required|in:Benefit,Cost', + ], [ + 'bobot.min' => 'Bobot tidak boleh 0.', + ]); + + // Simpan data kriteria baru ke database + Kriteria::create($request->only(['kode_kriteria', 'nama_kriteria', 'bobot', 'jenis'])); + + return redirect()->route('admin.kriteria.index')->with('success', 'Data Kriteria berhasil ditambahkan!'); + } + + // Menampilkan form edit data + public function edit($id) + { + // Cari data kriteria berdasarkan ID + $kriteria = Kriteria::findOrFail($id); + return view('admin.pages.kriteria.edit', compact('kriteria')); + } + + // Menyimpan perubahan data + public function update(Request $request, $id) + { + // Validasi inputan dari form edit + $request->validate([ + 'kode_kriteria' => "required|max:10|unique:datakriteria,kode_kriteria,$id,id", + 'nama_kriteria' => 'required|max:255', + 'bobot' => 'required|numeric|min:0.01|max:1', // nilai > 0 + 'jenis' => 'required|in:Benefit,Cost', + ], [ + 'bobot.min' => 'Bobot tidak boleh 0.', + ]); + + // Cari data kriteria yang akan diupdate + $kriteria = Kriteria::findOrFail($id); + // Update data dengan input yang diberikan + $kriteria->update($request->only(['kode_kriteria', 'nama_kriteria', 'bobot', 'jenis'])); + + return redirect()->route('admin.kriteria.index')->with('success', 'Data Kriteria berhasil diperbarui!'); + } + + // Menghapus data kriteria + public function destroy($id) + { + // Cari data kriteria berdasarkan ID + $kriteria = Kriteria::findOrFail($id); + // Hapus data dari database + $kriteria->delete(); + + return redirect()->route('admin.kriteria.index')->with('success', 'Data Kriteria berhasil dihapus!'); + } +} diff --git a/app/Http/Controllers/Admin/ForgotPasswordController.php b/app/Http/Controllers/Admin/ForgotPasswordController.php new file mode 100644 index 0000000..bb8d95c --- /dev/null +++ b/app/Http/Controllers/Admin/ForgotPasswordController.php @@ -0,0 +1,32 @@ +validate(['email' => 'required|email']); + + // Kirim link reset password ke email yang dimasukkan + $status = Password::sendResetLink( + $request->only('email') + ); + + // Jika berhasil mengirim link, tampilkan pesan status + // Jika gagal, kembalikan dengan pesan error pada field email + return $status === Password::RESET_LINK_SENT + ? back()->with('status', __($status)) //sukses + : back()->withErrors(['email' => __($status)]); //gagal + } +} diff --git a/app/Http/Controllers/Admin/PenilaianController.php b/app/Http/Controllers/Admin/PenilaianController.php new file mode 100644 index 0000000..503df8b --- /dev/null +++ b/app/Http/Controllers/Admin/PenilaianController.php @@ -0,0 +1,93 @@ +input('perPage', 10); // Jumlah data per halaman + $search = $request->input('search'); // Keyword pencaria + + $query = DataAlternatif::with(['penilaian.subkriteria']); // Relasi eager loading + + if ($search) { + // Jika terdapat pencarian, filter berdasarkan nama alternatif atau nama subkriteria + $query->where(function ($q) use ($search) { + $q->where('nama_alternatif', 'like', "%{$search}%") + ->orWhereHas('penilaian.subkriteria', function ($sub) use ($search) { + $sub->where('nama_subkriteria', 'like', "%{$search}%"); + }); + }); + } + + // Ambil data dengan pagination dan simpan parameter query kecuali halaman + $data = $query->paginate($perPage)->appends($request->except('page')); + + // Ambil semua data kriteria untuk ditampilkan + $kriteria = Kriteria::all(); + + return view('admin.pages.penilaian.index', compact('data', 'perPage', 'kriteria', 'search')); + } + + // Menampilkan form edit penilaian + public function edit($id) + { + $alternatif = DataAlternatif::findOrFail($id); // Ambil data alternatif berdasarkan ID + $penilaians = Penilaian::where('alternatif_id', $id)->get(); // Ambil data penilaian yang terkait + $kriteria = Kriteria::all(); // Ambil seluruh kriteria + + $nilai = []; // Menyimpan ID subkriteria per kriteria untuk ditampilkan di form + foreach ($penilaians as $penilaian) { + $nilai[$penilaian->kriteria_id] = $penilaian->subkriteria_id; + } + + return view('admin.pages.penilaian.edit', compact('alternatif', 'kriteria', 'nilai')); + } + + // Memperbarui data penilaian + public function update(Request $request, $id) + { + $alternatif = DataAlternatif::findOrFail($id); // Pastikan data alternatif ada + + // Validasi input + $request->validate([ + 'subkriteria_1' => 'required|numeric', + 'subkriteria_2' => 'required|numeric', + 'subkriteria_3' => 'required|numeric', + 'subkriteria_4' => 'required|numeric', + 'subkriteria_5' => 'required|numeric', + 'subkriteria_6' => 'required|numeric', + ]); + + // Lakukan update atau insert data penilaian untuk masing-masing kriteria + foreach (range(1, 6) as $i) { + $subkriteriaId = $request->input("subkriteria_$i"); // Ambil input subkriteria + $subkriteria = Subkriteria::find($subkriteriaId); // Cari data subkriteria + + if ($subkriteria) { + Penilaian::updateOrCreate( + [ + 'alternatif_id' => $alternatif->id, // Berdasarkan alternatif dan kriteria + 'kriteria_id' => $i, + ], + [ + 'subkriteria_id' => $subkriteria->id, // Simpan subkriteria yang dipilih + 'nilai' => $subkriteria->nilai, // Ambil nilai dari subkriteria + ] + ); + } + } + + return redirect()->route('admin.penilaian.index')->with('success', 'Penilaian berhasil diperbarui!'); + } +} diff --git a/app/Http/Controllers/Admin/PerhitunganController.php b/app/Http/Controllers/Admin/PerhitunganController.php new file mode 100644 index 0000000..d077fda --- /dev/null +++ b/app/Http/Controllers/Admin/PerhitunganController.php @@ -0,0 +1,103 @@ +showJenisPakaian('Dress'); } + public function blouse() { return $this->showJenisPakaian('Blouse'); } + public function cardigan() { return $this->showJenisPakaian('Cardigan'); } + public function rok() { return $this->showJenisPakaian('Rok'); } + public function celana() { return $this->showJenisPakaian('Celana'); } + + // Method umum untuk menampilkan data berdasarkan jenis pakaian + public function showJenisPakaian($jenis) + { + // Ambil data alternatif yang memiliki penilaian pada kriteria ke-3 (jenis pakaian), + // dan nilai subkriteria-nya sesuai dengan jenis yang dipilih (misalnya 'Dress') + $alternatif = DataAlternatif::with(['penilaian.subkriteria']) + ->whereHas('penilaian', function($query) use ($jenis) { + $query->where('kriteria_id', 3) + ->whereHas('subkriteria', function($subquery) use ($jenis) { + $subquery->where('nama_subkriteria', $jenis); + }); + }) + ->get(); + + // Ambil semua data kriteria + $kriteria = Kriteria::all(); + + // Hitung nilai maksimum dan minimum untuk tiap kriteria + $maxNilai = []; + $minNilai = []; + + foreach ($kriteria as $index => $k) { + $nilaiKriteria = []; + + foreach ($alternatif as $alt) { + $penilaian = $alt->penilaian->firstWhere('kriteria_id', $k->id); + if ($penilaian && $penilaian->subkriteria) { + $nilaiKriteria[] = $penilaian->subkriteria->nilai; + } + } + + $kode = 'C' . ($index + 1); + $maxNilai[$kode] = count($nilaiKriteria) > 0 ? max($nilaiKriteria) : 1; + $minNilai[$kode] = count($nilaiKriteria) > 0 ? min($nilaiKriteria) : 1; + } + + $normalisasi = []; + $pembobotan = []; + + foreach ($alternatif as $alt) { + $barisNormal = []; + $barisBobot = []; + + foreach ($kriteria as $index => $k) { + $penilaian = $alt->penilaian->firstWhere('kriteria_id', $k->id); + $nilai = $penilaian && $penilaian->subkriteria ? $penilaian->subkriteria->nilai : 0; + + $kode = 'C' . ($index + 1); + + if ($k->jenis == 'Cost') { + $normal = $nilai > 0 ? $minNilai[$kode] / $nilai : 0; + } else { + $normal = $maxNilai[$kode] > 0 ? $nilai / $maxNilai[$kode] : 0; + } + + $normalRounded = round($normal, 3); + $bobot = round($normalRounded * $k->bobot, 3); + + $barisNormal[$kode] = $normalRounded; + $barisBobot[$kode] = $bobot; + } + + $normalisasi[] = [ + 'nama' => $alt->nama_alternatif, + 'nilai' => $barisNormal + ]; + + $pembobotan[] = [ + 'nama' => $alt->nama_alternatif, + 'nilai' => $barisBobot + ]; + } + + $viewName = strtolower($jenis); + return view("admin.pages.perhitungan.$viewName", compact( + 'alternatif', + 'kriteria', + 'jenis', + 'normalisasi', + 'pembobotan' + )); + } +} diff --git a/app/Http/Controllers/Admin/ResetPasswordController.php b/app/Http/Controllers/Admin/ResetPasswordController.php new file mode 100644 index 0000000..d7cef7c --- /dev/null +++ b/app/Http/Controllers/Admin/ResetPasswordController.php @@ -0,0 +1,57 @@ + $token, + 'email' => $request->email + ]); + } + + public function reset(Request $request) + { + // Validasi input user + $request->validate([ + 'token' => 'required', + 'email' => 'required|email', + 'password' => 'required|min:8|confirmed', + ]); + + // Logging request untuk debugging (hindari menyimpan password di log dalam produksi) + Log::info('Reset Password Request:', $request->only('email', 'password', 'password_confirmation', 'token')); // Ganti \Log menjadi Log + + // Melakukan proses reset password menggunakan Laravel bawaan + $status = Password::reset( + $request->only('email', 'password', 'password_confirmation', 'token'), + function ($user, $password) { + // Jika berhasil, set password baru dan generate ulang remember_token + $user->forceFill([ + 'password' => Hash::make($password), + 'remember_token' => Str::random(60), + ])->save(); + } + ); + + // Logging status hasil reset + Log::info('Password Reset Status:', ['status' => $status]); // Ganti \Log menjadi Log + + // Jika berhasil reset, redirect ke halaman login dengan pesan sukses + if ($status === Password::PASSWORD_RESET) { + return redirect('/login')->with('status', 'Password berhasil direset. Silakan login.'); + } else { + // Jika gagal, kembalikan ke halaman sebelumnya dengan error message + return back()->withErrors(['email' => [__($status)]]); + } + } +} diff --git a/app/Http/Controllers/Admin/RiwayatController.php b/app/Http/Controllers/Admin/RiwayatController.php new file mode 100644 index 0000000..b7c72a9 --- /dev/null +++ b/app/Http/Controllers/Admin/RiwayatController.php @@ -0,0 +1,35 @@ +get('per_page', 5); // Ambil dari query string, default 10 + $riwayat = QuizHistory::orderBy('created_at', 'desc') + ->paginate($perPage) + ->withQueryString(); // penting agar per_page tetap ada saat klik pagination + + return view('admin.pages.riwayat.index', compact('riwayat')); + } + + public function destroy($id) + { + try { + $riwayat = QuizHistory::findOrFail($id); + $riwayat->delete(); + + return redirect()->back()->with('success', 'Riwayat berhasil dihapus.'); + } catch (\Exception $e) { + return redirect()->back()->with('error', 'Gagal menghapus riwayat.'); + } + } + +} diff --git a/app/Http/Controllers/Admin/SubKriteriaController.php b/app/Http/Controllers/Admin/SubKriteriaController.php new file mode 100644 index 0000000..abc323d --- /dev/null +++ b/app/Http/Controllers/Admin/SubKriteriaController.php @@ -0,0 +1,118 @@ +get(); // Periksa kolom yang digunakan + + // Kirim data ke view + return view('admin.pages.subkriteria.dynamic', compact('kriteria', 'subkriterias')); + } + + // Menampilkan form untuk membuat subkriteria + public function create($id) + { + // Ambil data kriteria berdasarkan ID + $kriteria = Kriteria::findOrFail($id); + + // Kirim data ke view + return view('admin.pages.subkriteria.create', compact('kriteria')); + } + + // Menyimpan data subkriteria baru + public function store(Request $request) + { + // Validasi input + $validated = $request->validate([ + 'kriteria_id' => 'required|exists:datakriteria,id', + 'name' => 'required|string|max:255', + 'nilai' => 'required|numeric|min:0.01', + ], [ + 'nilai.min' => 'Nilai tidak boleh 0.', + ]); + + // Simpan data subkriteria + SubKriteria::create([ + 'kriteria_id' => $validated['kriteria_id'], + 'nama_subkriteria' => $validated['name'], + 'nilai' => $validated['nilai'], + ]); + + // Redirect kembali ke halaman subkriteria berdasarkan kriteria_id + return redirect()->route('admin.pages.subkriteria.kriteria', ['id' => $validated['kriteria_id']]) + ->with('success', 'Subkriteria berhasil ditambahkan.'); + } + + public function edit($id) + { + // Ambil data subkriteria berdasarkan subkriteria_id + $subkriteria = SubKriteria::find($id); + + if (!$subkriteria) { + return redirect()->back()->with('error', 'Subkriteria tidak ditemukan.'); + } + + // Ambil data kriteria terkait + $kriteria = Kriteria::findOrFail($subkriteria->kriteria_id); + + // Kirim data ke view + return view('admin.pages.subkriteria.edit', compact('subkriteria', 'kriteria')); + } + + public function update(Request $request, $id) + { + // Validasi input + $validated = $request->validate([ + 'kriteria_id' => 'required|exists:datakriteria,id', + 'name' => 'required|string|max:255', + 'nilai' => 'required|numeric|min:0.01', + ], [ + 'nilai.min' => 'Nilai tidak boleh 0.', + ]); + + // Ambil data subkriteria berdasarkan ID + $subkriteria = SubKriteria::findOrFail($id); // Pastikan menggunakan subkriteria_id jika diperlukan + + // Update data subkriteria + $subkriteria->update([ + 'kriteria_id' => $validated['kriteria_id'], + 'nama_subkriteria' => $validated['name'], + 'nilai' => $validated['nilai'], + ]); + + // Redirect kembali ke halaman subkriteria berdasarkan kriteria_id + return redirect()->route('admin.pages.subkriteria.kriteria', ['id' => $validated['kriteria_id']]) + ->with('success', 'Subkriteria berhasil diperbarui.'); + } + + public function destroy($id) + { + // Cari subkriteria berdasarkan ID + $subkriteria = SubKriteria::find($id); + + // Jika tidak ditemukan, redirect balik dengan pesan error + if (!$subkriteria) { + return redirect()->back()->with('error', 'Subkriteria tidak ditemukan.'); + } + + // Hapus subkriteria + $subkriteria->delete(); + + // Redirect kembali dengan pesan sukses + return redirect()->back()->with('success', 'Subkriteria berhasil dihapus.'); + } + +} diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php new file mode 100644 index 0000000..e0a80f9 --- /dev/null +++ b/app/Http/Controllers/Admin/UserController.php @@ -0,0 +1,56 @@ +validate([ + 'name' => 'required|string|max:255', + 'email' => 'required|email|unique:users,email,' . $id, + 'username' => 'required|string|unique:users,username,' . $id, + 'password' => 'nullable|string|min:6|confirmed', // validasi password + konfirmasi + ]); + + // Ambil data yang akan diperbarui + $data = $request->only(['name', 'email', 'username']); + + // Jika password diisi, enkripsi dan tambahkan ke data + if ($request->filled('password')) { + $data['password'] = Hash::make($request->password); + } + + // Update data user di database + $user->update($data); + + return redirect()->route('user.show', $user->id)->with('success', 'User berhasil diperbarui'); + } + +} diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php new file mode 100644 index 0000000..77ec359 --- /dev/null +++ b/app/Http/Controllers/Controller.php @@ -0,0 +1,12 @@ +get(); + + return view('landingpage.home', [ + 'skorAkhir' => $this->getSkorAkhirByJenis('Dress'), + 'blouseSkorAkhir' => $this->getSkorAkhirByJenis('Blouse'), + 'cardiganSkorAkhir' => $this->getSkorAkhirByJenis('Cardigan'), + 'rokSkorAkhir' => $this->getSkorAkhirByJenis('Rok'), + 'celanaSkorAkhir' => $this->getSkorAkhirByJenis('Celana'), + 'kriteria' => $kriteria + ]); + } + + public function dress() { return $this->showJenisPakaian('Dress'); } + public function blouse() { return $this->showJenisPakaian('Blouse'); } + public function cardigan() { return $this->showJenisPakaian('Cardigan'); } + public function rok() { return $this->showJenisPakaian('Rok'); } + public function celana() { return $this->showJenisPakaian('Celana'); } + + private function showJenisPakaian($jenis, $preferensi = []) + { + $skorAkhir = $this->getSkorAkhirByJenis($jenis, $preferensi); + $alternatif = DataAlternatif::whereHas('penilaian.subkriteria', fn($q) => $q->where('nama_subkriteria', $jenis))->get(); + $kriteria = Kriteria::with('subkriteria')->get(); + + return view('landingpage.rekomendasi', compact('alternatif', 'kriteria', 'jenis', 'skorAkhir', 'preferensi')); + } + + private function getSkorAkhirByJenis($jenis, $preferensi = []) + { + $alternatif = DataAlternatif::with(['penilaian.subkriteria', 'penilaian.kriteria']) + ->whereHas('penilaian.subkriteria', function ($query) use ($jenis) { + $query->where('nama_subkriteria', $jenis); + }) + ->get(); + + $kriteria = Kriteria::with('subkriteria')->get(); + $skorAkhir = []; + + foreach ($alternatif as $alt) { + $total = 0; + foreach ($kriteria as $k) { + $penilaian = $alt->penilaian->where('kriteria_id', $k->id)->first(); + if (!$penilaian) continue; + + $nilaiAlt = $penilaian->subkriteria->nilai; + $nilaiPref = $preferensi[$this->mapKriteriaKey($k->nama_kriteria)]['nilai'] ?? $nilaiAlt; + + $normalisasi = 1 - abs($nilaiAlt - $nilaiPref); + $total += $normalisasi * $k->bobot; + } + + $skorAkhir[] = [ + 'alternatif' => $alt, + 'skor' => round($total, 3), + ]; + } + + usort($skorAkhir, fn($a, $b) => $b['skor'] <=> $a['skor']); + + return $skorAkhir; + } + + private function mapKriteriaKey($nama) + { + $nama = strtolower($nama); + return match ($nama) { + 'harga', 'cost' => 'harga', + 'jenis pakaian' => 'jenis', + 'warna pakaian' => 'warna', + 'lokasi acara' => 'lokasi', + 'cuaca acara' => 'cuaca', + 'jenis acara' => 'acara', + default => str_replace(' ', '_', $nama), + }; + } + + public function simpankuisionerdanrekomendasi(Request $request) + { + try { + $validated = $request->validate([ + 'jenis_acara' => 'required|string', + 'harga' => 'required|string', + 'jenis_pakaian' => 'required|string', + 'warna' => 'required|string', + 'cuaca' => 'required|string', + 'lokasi' => 'required|string', + ]); + + // Kumpulkan preferensi untuk diproses + $jawaban = [ + 'jenis_acara' => $validated['jenis_acara'], + 'harga' => $validated['harga'], + 'warna' => $validated['warna'], + 'cuaca' => $validated['cuaca'], + 'lokasi' => $validated['lokasi'], + ]; + + $preferensi = $this->prosesPreferensiBerdasarkanSubkriteria($jawaban); + $skorAkhir = $this->getSkorAkhirByJenis($validated['jenis_pakaian'], $preferensi); + + // Simpan ke database + $quizHistory = new QuizHistory(); + $quizHistory->data_kuisioner = json_encode($jawaban); + $quizHistory->hasil_rekomendasi = json_encode($skorAkhir); + $quizHistory->save(); + + return $this->showJenisPakaian($validated['jenis_pakaian'], $preferensi); + + } catch (\Exception $e) { + Log::error('Error simpan kuis: ' . $e->getMessage()); + return back()->with('error', 'Terjadi kesalahan. Silakan coba lagi.'); + } + } + + private function prosesPreferensiBerdasarkanSubkriteria($jawaban) + { + $preferensi = []; + + foreach ($jawaban as $nama_kriteria => $nama_subkriteria) { + $kriteria = Kriteria::where('nama_kriteria', $nama_kriteria)->first(); + $subkriteria = Subkriteria::where('nama_subkriteria', $nama_subkriteria) + ->where('kriteria_id', $kriteria->id ?? null) + ->first(); + + if ($kriteria && $subkriteria) { + $key = $this->mapKriteriaKey($kriteria->nama_kriteria); + + $preferensi[$key] = [ + 'value' => $subkriteria->nama_subkriteria, + 'nilai' => $subkriteria->nilai, + 'bobot' => $kriteria->bobot + ]; + + if ($key === 'harga') { + $preferensi[$key]['range'] = $subkriteria->nama_subkriteria; + } + } + } + + return $preferensi; + } + + + // Menampilkan form kuis + public function showKuis($jenis = 'Dress') + { + $kriteria = Kriteria::with(['subkriteria' => function($query) { + $query->orderBy('nilai', 'desc'); + }])->get(); + + return view('landingpage.pilihpakaian', compact('kriteria', 'jenis')); + } +} diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php new file mode 100644 index 0000000..494c050 --- /dev/null +++ b/app/Http/Kernel.php @@ -0,0 +1,68 @@ + + */ + protected $middleware = [ + // \App\Http\Middleware\TrustHosts::class, + \App\Http\Middleware\TrustProxies::class, + \Illuminate\Http\Middleware\HandleCors::class, + \App\Http\Middleware\PreventRequestsDuringMaintenance::class, + \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, + \App\Http\Middleware\TrimStrings::class, + \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, + ]; + + /** + * The application's route middleware groups. + * + * @var array`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\n\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n\n// Abbreviations\n//\n// 1. Duplicate behavior to the data-bs-* attribute for our tooltip plugin\n// 2. Add the correct text decoration in Chrome, Edge, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Prevent the text-decoration to be skipped.\n\nabbr[title],\nabbr[data-bs-original-title] { // 1\n text-decoration: underline; // 2\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n text-decoration-skip-ink: none; // 4\n}\n\n\n// Address\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\n\n// Lists\n\nol,\nul {\n padding-left: 2rem;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\n// 1. Undo browser default\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // 1\n}\n\n\n// Blockquote\n\nblockquote {\n margin: 0 0 1rem;\n}\n\n\n// Strong\n//\n// Add the correct font weight in Chrome, Edge, and Safari\n\nb,\nstrong {\n font-weight: $font-weight-bolder;\n}\n\n\n// Small\n//\n// Add the correct font size in all browsers\n\nsmall {\n @include font-size($small-font-size);\n}\n\n\n// Mark\n\nmark {\n padding: $mark-padding;\n background-color: $mark-bg;\n}\n\n\n// Sub and Sup\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n\nsub,\nsup {\n position: relative;\n @include font-size($sub-sup-font-size);\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n// Links\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n\n &:hover {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]):not([class]) {\n &,\n &:hover {\n color: inherit;\n text-decoration: none;\n }\n}\n\n\n// Code\n\npre,\ncode,\nkbd,\nsamp {\n font-family: $font-family-code;\n @include font-size(1em); // Correct the odd `em` font sizing in all browsers.\n direction: ltr #{\"/* rtl:ignore */\"};\n unicode-bidi: bidi-override;\n}\n\n// 1. Remove browser default top margin\n// 2. Reset browser default of `1em` to use `rem`s\n// 3. Don't allow content to break outside\n\npre {\n display: block;\n margin-top: 0; // 1\n margin-bottom: 1rem; // 2\n overflow: auto; // 3\n @include font-size($code-font-size);\n color: $pre-color;\n\n // Account for some code outputs that place code tags in pre tags\n code {\n @include font-size(inherit);\n color: inherit;\n word-break: normal;\n }\n}\n\ncode {\n @include font-size($code-font-size);\n color: $code-color;\n word-wrap: break-word;\n\n // Streamline the style when inside anchors to avoid broken underline and more\n a > & {\n color: inherit;\n }\n}\n\nkbd {\n padding: $kbd-padding-y $kbd-padding-x;\n @include font-size($kbd-font-size);\n color: $kbd-color;\n background-color: $kbd-bg;\n @include border-radius($border-radius-sm);\n\n kbd {\n padding: 0;\n @include font-size(1em);\n font-weight: $nested-kbd-font-weight;\n }\n}\n\n\n// Figures\n//\n// Apply a consistent margin strategy (matches our type styles).\n\nfigure {\n margin: 0 0 1rem;\n}\n\n\n// Images and content\n\nimg,\nsvg {\n vertical-align: middle;\n}\n\n\n// Tables\n//\n// Prevent double borders\n\ntable {\n caption-side: bottom;\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: $table-cell-padding-y;\n padding-bottom: $table-cell-padding-y;\n color: $table-caption-color;\n text-align: left;\n}\n\n// 1. Removes font-weight bold by inheriting\n// 2. Matches default `