diff --git a/.phpunit.result.cache b/.phpunit.result.cache index 022401b..8c75091 100644 --- a/.phpunit.result.cache +++ b/.phpunit.result.cache @@ -1 +1 @@ -{"version":2,"defects":[],"times":{"Tests\\Feature\\RekomendasiTest::test_high_math_and_coding_prefers_teknologi_informasi":17.81,"Tests\\Feature\\RekomendasiTest::test_high_language_prefers_bahasa_komunikasi":0.186}} \ No newline at end of file +{"version":2,"defects":{"Tests\\Unit\\RekomendasiAlgorithmTest::test_prestasi_scoring_tinggi":7,"Tests\\Unit\\RekomendasiAlgorithmTest::test_prestasi_scoring_sedang":7,"Tests\\Feature\\Auth\\RegistrationTest::test_new_users_can_register":7,"Tests\\Feature\\RekomendasiTest::test_high_math_and_coding_prefers_teknologi_informasi":7,"Tests\\Feature\\RekomendasiTest::test_high_language_prefers_bahasa_komunikasi":7,"Tests\\Feature\\ExplainableRecommendationTest::test_explanation_contains_meaningful_text":7,"Tests\\Feature\\ExplainableRecommendationTest::test_explanation_displayed_in_view":7,"Tests\\Feature\\Auth\\PasswordResetMessageTest::test_forgot_password_shows_indonesian_error_for_invalid_email":7,"Tests\\Feature\\Auth\\PasswordResetMessageTest::test_forgot_password_form_displays_error":8},"times":{"Tests\\Feature\\RekomendasiTest::test_high_math_and_coding_prefers_teknologi_informasi":0.064,"Tests\\Feature\\RekomendasiTest::test_high_language_prefers_bahasa_komunikasi":0.069,"Tests\\Unit\\ExampleTest::test_that_true_is_true":0.013,"Tests\\Unit\\RekomendasiAlgorithmTest::test_minat_mapping_logika_komputer":0.025,"Tests\\Unit\\RekomendasiAlgorithmTest::test_minat_mapping_alam_tanaman":0,"Tests\\Unit\\RekomendasiAlgorithmTest::test_minat_mapping_bisnis":0,"Tests\\Unit\\RekomendasiAlgorithmTest::test_nilai_kategori_tinggi":0,"Tests\\Unit\\RekomendasiAlgorithmTest::test_nilai_kategori_sedang":0,"Tests\\Unit\\RekomendasiAlgorithmTest::test_nilai_kategori_rendah":0.001,"Tests\\Unit\\RekomendasiAlgorithmTest::test_prestasi_scoring_tinggi":0.003,"Tests\\Unit\\RekomendasiAlgorithmTest::test_prestasi_scoring_sedang":0,"Tests\\Unit\\RekomendasiAlgorithmTest::test_prestasi_scoring_minimal":0.001,"Tests\\Feature\\Auth\\AuthenticationTest::test_login_screen_can_be_rendered":0.092,"Tests\\Feature\\Auth\\AuthenticationTest::test_users_can_authenticate_using_the_login_screen":0.184,"Tests\\Feature\\Auth\\AuthenticationTest::test_users_can_not_authenticate_with_invalid_password":0.234,"Tests\\Feature\\Auth\\AuthenticationTest::test_users_can_logout":0.022,"Tests\\Feature\\Auth\\EmailVerificationTest::test_email_verification_screen_can_be_rendered":0.024,"Tests\\Feature\\Auth\\EmailVerificationTest::test_email_can_be_verified":0.019,"Tests\\Feature\\Auth\\EmailVerificationTest::test_email_is_not_verified_with_invalid_hash":0.026,"Tests\\Feature\\Auth\\PasswordConfirmationTest::test_confirm_password_screen_can_be_rendered":0.042,"Tests\\Feature\\Auth\\PasswordConfirmationTest::test_password_can_be_confirmed":0.113,"Tests\\Feature\\Auth\\PasswordConfirmationTest::test_password_is_not_confirmed_with_invalid_password":0.246,"Tests\\Feature\\Auth\\PasswordResetTest::test_reset_password_link_screen_can_be_rendered":0.018,"Tests\\Feature\\Auth\\PasswordResetTest::test_reset_password_link_can_be_requested":0.047,"Tests\\Feature\\Auth\\PasswordResetTest::test_reset_password_screen_can_be_rendered":0.034,"Tests\\Feature\\Auth\\PasswordResetTest::test_password_can_be_reset_with_valid_token":0.04,"Tests\\Feature\\Auth\\PasswordUpdateTest::test_password_can_be_updated":0.12,"Tests\\Feature\\Auth\\PasswordUpdateTest::test_correct_password_must_be_provided_to_update_password":0.094,"Tests\\Feature\\Auth\\RegistrationTest::test_registration_screen_can_be_rendered":0.01,"Tests\\Feature\\Auth\\RegistrationTest::test_new_users_can_register":0.016,"Tests\\Feature\\ExampleTest::test_the_application_returns_a_successful_response":0.006,"Tests\\Feature\\ProfileTest::test_profile_page_is_displayed":0.016,"Tests\\Feature\\ProfileTest::test_profile_information_can_be_updated":0.024,"Tests\\Feature\\ProfileTest::test_email_verification_status_is_unchanged_when_the_email_address_is_unchanged":0.022,"Tests\\Feature\\ProfileTest::test_user_can_delete_their_account":0.097,"Tests\\Feature\\ProfileTest::test_correct_password_must_be_provided_to_delete_account":0.095,"Tests\\Feature\\CrudValidationTest::test_admin_can_add_jurusan_data":0.027,"Tests\\Feature\\CrudValidationTest::test_bk_can_add_jurusan_data":0.02,"Tests\\Feature\\CrudValidationTest::test_admin_guru_bk_store_validates_email_and_password":0.014,"Tests\\Feature\\CrudValidationTest::test_rekomendasi_ipa_requires_all_ipa_scores":0.018,"Tests\\Feature\\CrudValidationTest::test_admin_student_detail_only_accepts_siswa_id":0.022,"Tests\\Feature\\ExplainableRecommendationTest::test_recommendation_includes_explanation":0.038,"Tests\\Feature\\ExplainableRecommendationTest::test_explanation_contains_meaningful_text":0.029,"Tests\\Feature\\ExplainableRecommendationTest::test_scoring_detail_stored_correctly":0.027,"Tests\\Feature\\ExplainableRecommendationTest::test_all_recommendations_have_explanations":0.03,"Tests\\Feature\\ExplainableRecommendationTest::test_explanation_displayed_in_view":0.022,"Tests\\Feature\\Auth\\PasswordResetMessageTest::test_forgot_password_shows_indonesian_error_for_invalid_email":0.059,"Tests\\Feature\\Auth\\PasswordResetMessageTest::test_forgot_password_form_displays_error":0.019}} \ No newline at end of file diff --git a/app/Http/Controllers/AdminController.php b/app/Http/Controllers/AdminController.php index e616845..2990ecf 100644 --- a/app/Http/Controllers/AdminController.php +++ b/app/Http/Controllers/AdminController.php @@ -86,7 +86,7 @@ public function students(Request $request) public function studentDetail($id) { - $student = User::findOrFail($id); + $student = User::where('role', 'siswa')->findOrFail($id); $recommendations = Recommendation::where('user_id', $id) ->orderBy('created_at', 'desc') ->get(); @@ -99,7 +99,7 @@ public function studentDetail($id) public function chatHistory($id) { - $user = User::findOrFail($id); + $user = User::where('role', 'siswa')->findOrFail($id); $chatHistories = ChatHistory::where('user_id', $id) ->orderBy('created_at', 'asc') ->get(); @@ -380,16 +380,12 @@ public function updateProfil(Request $request) public function updatePassword(Request $request) { $request->validate([ - 'current_password' => 'required', + 'current_password' => 'required|current_password', 'password' => 'required|string|min:8|confirmed', ]); $admin = Auth::user(); - if (!Hash::check($request->current_password, $admin->password)) { - return back()->withErrors(['current_password' => 'Password lama salah.']); - } - $admin->password = Hash::make($request->password); $admin->save(); diff --git a/app/Http/Controllers/AlumniController.php b/app/Http/Controllers/AlumniController.php index c6941e5..a8c3a7b 100644 --- a/app/Http/Controllers/AlumniController.php +++ b/app/Http/Controllers/AlumniController.php @@ -49,7 +49,7 @@ public function store(Request $request) // Non-akademik 'minat' => 'nullable|string|max:255', 'cita_cita' => 'nullable|string|max:255', - 'preferensi_studi' => 'nullable|in:Praktik_Langsung,DuDi,Project_Based,Blended', + 'preferensi_studi' => 'nullable|in:Praktik_Langsung,DuDi,Project_Based,Blended,Praktik Langsung,Project Based', 'prestasi' => 'nullable|string|max:255', // Major & Outcome @@ -59,6 +59,10 @@ public function store(Request $request) 'catatan' => 'nullable|string|max:500', ]); + if (!empty($validated['preferensi_studi'])) { + $validated['preferensi_studi'] = str_replace(['Praktik Langsung', 'Project Based'], ['Praktik_Langsung', 'Project_Based'], $validated['preferensi_studi']); + } + Alumni::create($validated); return redirect()->route('admin.alumni.index')->with('success', 'Alumni berhasil ditambahkan'); @@ -101,7 +105,7 @@ public function update(Request $request, Alumni $alumni) 'minat' => 'nullable|string|max:255', 'cita_cita' => 'nullable|string|max:255', - 'preferensi_studi' => 'nullable|in:Praktik_Langsung,DuDi,Project_Based,Blended', + 'preferensi_studi' => 'nullable|in:Praktik_Langsung,DuDi,Project_Based,Blended,Praktik Langsung,Project Based', 'prestasi' => 'nullable|string|max:255', 'major_masuk' => 'required|string|max:255', @@ -110,6 +114,10 @@ public function update(Request $request, Alumni $alumni) 'catatan' => 'nullable|string|max:500', ]); + if (!empty($validated['preferensi_studi'])) { + $validated['preferensi_studi'] = str_replace(['Praktik Langsung', 'Project Based'], ['Praktik_Langsung', 'Project_Based'], $validated['preferensi_studi']); + } + $alumni->update($validated); return redirect()->route('admin.alumni.index')->with('success', 'Alumni berhasil diupdate'); diff --git a/app/Http/Controllers/BKController.php b/app/Http/Controllers/BKController.php index 79d9f68..534445a 100644 --- a/app/Http/Controllers/BKController.php +++ b/app/Http/Controllers/BKController.php @@ -86,7 +86,7 @@ public function students(Request $request) public function studentDetail($id) { - $student = User::findOrFail($id); + $student = User::where('role', 'siswa')->findOrFail($id); $recommendations = Recommendation::where('user_id', $id) ->orderBy('created_at', 'desc') ->get(); @@ -99,7 +99,7 @@ public function studentDetail($id) public function chatHistory($id) { - $user = User::findOrFail($id); + $user = User::where('role', 'siswa')->findOrFail($id); $chatHistories = ChatHistory::where('user_id', $id) ->orderBy('created_at', 'asc') ->get(); @@ -301,16 +301,12 @@ public function updateProfil(Request $request) public function updatePassword(Request $request) { $request->validate([ - 'current_password' => 'required', + 'current_password' => 'required|current_password', 'password' => 'required|string|min:8|confirmed', ]); $guru = Auth::user(); - if (!Hash::check($request->current_password, $guru->password)) { - return back()->withErrors(['current_password' => 'Password lama salah.']); - } - $guru->password = Hash::make($request->password); $guru->save(); diff --git a/app/Http/Controllers/RekomendasiController.php b/app/Http/Controllers/RekomendasiController.php index afc41ff..71e7fdf 100644 --- a/app/Http/Controllers/RekomendasiController.php +++ b/app/Http/Controllers/RekomendasiController.php @@ -26,6 +26,67 @@ public function index() return view('rekomendasi.input', compact('student')); } + /** + * Generate textual explanation untuk setiap kriteria + * Menjelaskan "mengapa jurusan ini cocok" berdasarkan scoring detail + */ + private function generateExplanation($jurusanNama, $detail, $katNilai, $kategoriMinat, $prefStudi, $prestasi) + { + $explanations = []; + + // 1. Penjelasan Nilai Akademik + $skorNilai = $detail['nilai'] ?? 0; + if ($skorNilai >= 0.8) { + $explanations['nilai'] = "✅ Nilai akademik Anda ($katNilai) sangat sesuai dengan jalur pendidikan yang dibutuhkan jurusan ini."; + } elseif ($skorNilai >= 0.6) { + $explanations['nilai'] = "✓ Nilai akademik Anda ($katNilai) cukup sesuai dengan persyaratan jurusan ini."; + } else { + $explanations['nilai'] = "⚠️ Nilai akademik Anda ($katNilai) masih perlu ditingkatkan untuk optimal di jurusan ini, namun tetap relevan."; + } + + // 2. Penjelasan Minat + $skorMinat = $detail['minat'] ?? 0; + if ($skorMinat >= 0.8) { + $explanations['minat'] = "✅ Minat Anda sangat sesuai dan cocok dengan fokus kurikulum $jurusanNama."; + } elseif ($skorMinat >= 0.6) { + $explanations['minat'] = "✓ Minat Anda cukup relevan dan sesuai dengan area pembelajaran di $jurusanNama."; + } else { + $explanations['minat'] = "ℹ️ Minat Anda memiliki kesamaan dan relevansi dengan aspek-aspek tertentu di $jurusanNama."; + } + + // 3. Penjelasan Preferensi Studi + $skorPref = $detail['pref'] ?? 0; + if ($skorPref >= 0.8) { + $explanations['pref'] = "✅ Metode pembelajaran \"$prefStudi\" yang Anda pilih sangat sesuai dengan pendekatan pembelajaran $jurusanNama."; + } elseif ($skorPref >= 0.6) { + $explanations['pref'] = "✓ Preferensi studi \"$prefStudi\" Anda cocok dengan sistem pembelajaran yang diterapkan."; + } else { + $explanations['pref'] = "ℹ️ Jurusan ini menawarkan elemen pembelajaran \"$prefStudi\" yang relevan dengan preferensi Anda."; + } + + // 4. Penjelasan Cita-cita + $skorCita = $detail['cita'] ?? 0; + if ($skorCita >= 0.8) { + $explanations['cita'] = "✅ Cita-cita karir Anda sangat sesuai dan aligned dengan standar lulusan bidang ini."; + } elseif ($skorCita >= 0.6) { + $explanations['cita'] = "✓ Cita-cita Anda memiliki potensi besar untuk dicapai melalui jurusan ini."; + } else { + $explanations['cita'] = "ℹ️ Jurusan ini membuka jalur karir yang sesuai dengan cita-cita dan aspirasi Anda."; + } + + // 5. Penjelasan Prestasi + $skorPrestasi = $detail['prestasi'] ?? 0; + if ($skorPrestasi >= 0.7) { + $explanations['prestasi'] = "✅ Prestasi Anda mencerminkan potensi kuat untuk sukses dan berkembang di jurusan ini."; + } elseif ($skorPrestasi >= 0.4) { + $explanations['prestasi'] = "✓ Prestasi Anda menunjukkan kemampuan dasar yang memadai dan relevan."; + } else { + $explanations['prestasi'] = "ℹ️ Prestasi tidak menjadi hambatan untuk mengembangkan diri dan berkembang di jurusan ini."; + } + + return $explanations; + } + /** * ============================================================ * ALGORITMA NAIVE BAYES UNTUK REKOMENDASI JURUSAN @@ -42,16 +103,57 @@ public function index() */ public function proses(Request $request) { + $validated = $request->validate([ + 'mtk' => ['nullable', 'numeric', 'min:0', 'max:100'], + 'fisika' => ['nullable', 'numeric', 'min:0', 'max:100'], + 'kimia' => ['nullable', 'numeric', 'min:0', 'max:100'], + 'biologi' => ['nullable', 'numeric', 'min:0', 'max:100'], + 'ekonomi' => ['nullable', 'numeric', 'min:0', 'max:100'], + 'geografi' => ['nullable', 'numeric', 'min:0', 'max:100'], + 'sosiologi' => ['nullable', 'numeric', 'min:0', 'max:100'], + 'sejarah' => ['nullable', 'numeric', 'min:0', 'max:100'], + 'minat' => ['required', 'string', 'max:255'], + 'pref_studi' => ['required', 'string', 'in:Sains & Teknologi,Pertanian & Lingkungan,Kesehatan & Ilmu Hayat,Bisnis & Manajemen,Sosial & Humaniora,Praktikum,Teori'], + 'cita_cita' => ['required', 'string', 'max:255'], + 'prestasi' => ['nullable', 'string', 'max:255'], + ]); + + $kelompokAsal = Auth::user()?->kelompok_asal; + if ($kelompokAsal === 'IPA') { + $request->validate([ + 'mtk' => ['required', 'numeric', 'min:0', 'max:100'], + 'fisika' => ['required', 'numeric', 'min:0', 'max:100'], + 'kimia' => ['required', 'numeric', 'min:0', 'max:100'], + 'biologi' => ['required', 'numeric', 'min:0', 'max:100'], + ]); + } elseif ($kelompokAsal === 'IPS') { + $request->validate([ + 'ekonomi' => ['required', 'numeric', 'min:0', 'max:100'], + 'geografi' => ['required', 'numeric', 'min:0', 'max:100'], + 'sosiologi' => ['required', 'numeric', 'min:0', 'max:100'], + 'sejarah' => ['required', 'numeric', 'min:0', 'max:100'], + ]); + } + $epsilon = 1e-9; // ================================================================ // LANGKAH 1: INPUT DATA // ================================================================ - $scores = $request->only(['mtk', 'fisika', 'kimia', 'biologi', 'ekonomi', 'geografi', 'sosiologi', 'sejarah']); - $minatRaw = strtolower(trim($request->minat ?? '')); - $prefStudi = $request->pref_studi ?? 'Sains & Teknologi'; - $citaRaw = strtolower(trim($request->cita_cita ?? '')); - $prestasiRaw = strtolower(trim($request->prestasi ?? '')); + $scores = [ + 'mtk' => $validated['mtk'] ?? null, + 'fisika' => $validated['fisika'] ?? null, + 'kimia' => $validated['kimia'] ?? null, + 'biologi' => $validated['biologi'] ?? null, + 'ekonomi' => $validated['ekonomi'] ?? null, + 'geografi' => $validated['geografi'] ?? null, + 'sosiologi' => $validated['sosiologi'] ?? null, + 'sejarah' => $validated['sejarah'] ?? null, + ]; + $minatRaw = strtolower(trim($validated['minat'] ?? '')); + $prefStudi = $validated['pref_studi'] ?? 'Sains & Teknologi'; + $citaRaw = strtolower(trim($validated['cita_cita'] ?? '')); + $prestasiRaw = strtolower(trim($validated['prestasi'] ?? '')); // ================================================================ // LANGKAH 2: PREPROCESSING DATA @@ -178,10 +280,20 @@ public function proses(Request $request) $hasilAkhir = []; foreach ($expVals as $namaJurusan => $val) { $prob = $val / max($sumExp, $epsilon); + $detail = $detailPerJurusan[$namaJurusan]; + $explanations = $this->generateExplanation( + $namaJurusan, + $detail, + $katNilai, + $minatRaw, + $prefStudi, + $prestasiRaw + ); $hasilAkhir[] = [ 'jurusan' => $namaJurusan, 'skor' => round($prob, 4), - 'detail' => $detailPerJurusan[$namaJurusan], + 'detail' => $detail, + 'explanation' => $explanations, 'kecocokan_nilai' => $katNilai, 'kecocokan_minat' => $minatRaw, 'kecocokan_pref' => $prefStudi, @@ -200,18 +312,18 @@ public function proses(Request $request) if ($user) { $savedRec = Recommendation::create([ 'user_id' => $user->id, - 'mtk' => $request->mtk ?? null, - 'fisika' => $request->fisika ?? null, - 'kimia' => $request->kimia ?? null, - 'biologi' => $request->biologi ?? null, - 'ekonomi' => $request->ekonomi ?? null, - 'geografi' => $request->geografi ?? null, - 'sosiologi' => $request->sosiologi ?? null, - 'sejarah' => $request->sejarah ?? null, - 'minat' => $request->minat ?? null, - 'preferensi_studi' => $request->pref_studi ?? null, - 'cita_cita' => $request->cita_cita ?? null, - 'prestasi' => $request->prestasi ?? null, + 'mtk' => $validated['mtk'] ?? null, + 'fisika' => $validated['fisika'] ?? null, + 'kimia' => $validated['kimia'] ?? null, + 'biologi' => $validated['biologi'] ?? null, + 'ekonomi' => $validated['ekonomi'] ?? null, + 'geografi' => $validated['geografi'] ?? null, + 'sosiologi' => $validated['sosiologi'] ?? null, + 'sejarah' => $validated['sejarah'] ?? null, + 'minat' => $validated['minat'], + 'preferensi_studi' => $validated['pref_studi'], + 'cita_cita' => $validated['cita_cita'], + 'prestasi' => $validated['prestasi'] ?? '', 'hasil_rekomendasi' => $hasilAkhir, ]); } @@ -377,6 +489,8 @@ private function normalisasiProbabilitas(float $value, float $min = 0.10, float */ private function hitungSkorPrestasi(string $prestasiRaw): float { + $prestasiRaw = strtolower(trim($prestasiRaw)); + if (empty($prestasiRaw)) { return 0.0; } diff --git a/lang/id/passwords.php b/lang/id/passwords.php new file mode 100644 index 0000000..988efaf --- /dev/null +++ b/lang/id/passwords.php @@ -0,0 +1,10 @@ + 'Password harus minimal 8 karakter dan sama dengan konfirmasi password.', + 'reset' => 'Password Anda telah berhasil direset!', + 'sent' => 'Kami telah mengirimkan tautan reset password lewat email Anda.', + 'throttled' => 'Mohon tunggu sebelum mencoba lagi.', + 'token' => 'Token reset password ini tidak valid.', + 'user' => 'Kami tidak dapat menemukan user dengan email address tersebut.', +]; diff --git a/resources/views/rekomendasi/hasil.blade.php b/resources/views/rekomendasi/hasil.blade.php index 47890f6..aa75ed0 100644 --- a/resources/views/rekomendasi/hasil.blade.php +++ b/resources/views/rekomendasi/hasil.blade.php @@ -215,9 +215,49 @@ + +
+ Nilai Akademik: {{ $topRecommendation['explanation']['nilai'] ?? '-' }} +
++ Minat & Bakat: {{ $topRecommendation['explanation']['minat'] ?? '-' }} +
++ Metode Pembelajaran: {{ $topRecommendation['explanation']['pref'] ?? '-' }} +
++ Cita-cita Karir: {{ $topRecommendation['explanation']['cita'] ?? '-' }} +
++ Prestasi Akademik: {{ $topRecommendation['explanation']['prestasi'] ?? '-' }} +
+Berdasarkan profil Anda dengan nilai akademik {{ $katNilai }} (rata-rata {{ number_format($average, 1) }}) dan preferensi studi {{ $prefStudi }}, @@ -236,6 +276,113 @@
{{ $rec['jurusan'] ?? '-' }}
+Skor: {{ number_format(($rec['skor'] ?? 0) * 100, 1) }}%
+Scoring per Kriteria:
+💡 Alasan Cocok:
+