fix: add Indonesian translations for password reset messages - fixes 'passwords.user' key display
This commit is contained in:
parent
e0b0c10ddc
commit
d208d68ad8
File diff suppressed because one or more lines are too long
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'password' => '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.',
|
||||
];
|
||||
|
|
@ -215,9 +215,49 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Explanation Breakdown -->
|
||||
<div class="mb-4 sm:mb-6 p-3 sm:p-4 bg-blue-50 rounded-lg border border-blue-200">
|
||||
<h4 class="font-bold text-blue-900 text-sm sm:text-base mb-3 sm:mb-4 flex items-center gap-2">
|
||||
<span class="text-lg">💡</span> Alasan Kenapa Jurusan Ini Cocok:
|
||||
</h4>
|
||||
|
||||
<div class="space-y-2 sm:space-y-3">
|
||||
<div class="flex gap-2 sm:gap-3">
|
||||
<span class="text-lg flex-shrink-0">📊</span>
|
||||
<p class="text-xs sm:text-sm text-gray-800">
|
||||
<strong>Nilai Akademik:</strong> {{ $topRecommendation['explanation']['nilai'] ?? '-' }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-2 sm:gap-3">
|
||||
<span class="text-lg flex-shrink-0">❤️</span>
|
||||
<p class="text-xs sm:text-sm text-gray-800">
|
||||
<strong>Minat & Bakat:</strong> {{ $topRecommendation['explanation']['minat'] ?? '-' }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-2 sm:gap-3">
|
||||
<span class="text-lg flex-shrink-0">🎓</span>
|
||||
<p class="text-xs sm:text-sm text-gray-800">
|
||||
<strong>Metode Pembelajaran:</strong> {{ $topRecommendation['explanation']['pref'] ?? '-' }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-2 sm:gap-3">
|
||||
<span class="text-lg flex-shrink-0">🎯</span>
|
||||
<p class="text-xs sm:text-sm text-gray-800">
|
||||
<strong>Cita-cita Karir:</strong> {{ $topRecommendation['explanation']['cita'] ?? '-' }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-2 sm:gap-3">
|
||||
<span class="text-lg flex-shrink-0">🏆</span>
|
||||
<p class="text-xs sm:text-sm text-gray-800">
|
||||
<strong>Prestasi Akademik:</strong> {{ $topRecommendation['explanation']['prestasi'] ?? '-' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kesimpulan -->
|
||||
<div class="p-3 sm:p-4 bg-yellow-50 rounded-lg border-l-4 border-yellow-400 mb-3 sm:mb-4">
|
||||
<h4 class="font-bold text-maroon mb-2 text-sm sm:text-base">Penjelasan:</h4>
|
||||
<h4 class="font-bold text-maroon mb-2 text-sm sm:text-base">📋 Kesimpulan:</h4>
|
||||
<p class="text-gray-700 text-xs sm:text-sm leading-relaxed">
|
||||
Berdasarkan profil Anda dengan <strong>nilai akademik {{ $katNilai }} (rata-rata {{ number_format($average, 1) }})</strong>
|
||||
dan <strong>preferensi studi {{ $prefStudi }}</strong>,
|
||||
|
|
@ -236,6 +276,113 @@
|
|||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Rekomendasi Alternatif & Penjelasan Detail -->
|
||||
@if(count($hasilAkhir) > 1)
|
||||
<div class="bg-white rounded-lg shadow-lg p-5 sm:p-8 mb-6 sm:mb-8">
|
||||
<h3 class="text-lg sm:text-xl font-bold text-maroon mb-4 sm:mb-6 flex items-center gap-2">
|
||||
<span class="text-2xl">🔍</span> Rekomendasi Alternatif & Penjelasan Detail
|
||||
</h3>
|
||||
|
||||
<div class="space-y-3 sm:space-y-4">
|
||||
@foreach($hasilAkhir as $index => $rec)
|
||||
@if($index > 0)
|
||||
<details class="border border-gray-300 rounded-lg overflow-hidden">
|
||||
<summary class="cursor-pointer p-4 hover:bg-gray-50 flex justify-between items-center">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-gray-200 text-gray-700 font-bold text-sm">
|
||||
{{ $index + 1 }}
|
||||
</span>
|
||||
<div>
|
||||
<p class="font-semibold text-gray-900 text-sm sm:text-base">{{ $rec['jurusan'] ?? '-' }}</p>
|
||||
<p class="text-xs sm:text-sm text-gray-600">Skor: {{ number_format(($rec['skor'] ?? 0) * 100, 1) }}%</p>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-gray-400">▸</span>
|
||||
</summary>
|
||||
|
||||
<div class="p-4 bg-gray-50 border-t border-gray-300 space-y-4">
|
||||
<!-- Score Breakdown -->
|
||||
<div class="bg-white p-3 rounded-lg">
|
||||
<p class="text-xs sm:text-sm font-bold text-gray-700 mb-3">Scoring per Kriteria:</p>
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between items-center text-xs">
|
||||
<span class="text-gray-600">📊 Nilai (40%)</span>
|
||||
<span class="font-semibold text-maroon">{{ number_format(($rec['detail']['nilai'] ?? 0) * 100, 1) }}%</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded h-1.5">
|
||||
<div class="bg-red-500 rounded h-1.5 transition-all" style="width: {{ number_format(($rec['detail']['nilai'] ?? 0) * 100, 1) }}%"></div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center text-xs mt-2">
|
||||
<span class="text-gray-600">❤️ Minat (35%)</span>
|
||||
<span class="font-semibold text-maroon">{{ number_format(($rec['detail']['minat'] ?? 0) * 100, 1) }}%</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded h-1.5">
|
||||
<div class="bg-pink-500 rounded h-1.5 transition-all" style="width: {{ number_format(($rec['detail']['minat'] ?? 0) * 100, 1) }}%"></div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center text-xs mt-2">
|
||||
<span class="text-gray-600">🎓 Preferensi (15%)</span>
|
||||
<span class="font-semibold text-maroon">{{ number_format(($rec['detail']['pref'] ?? 0) * 100, 1) }}%</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded h-1.5">
|
||||
<div class="bg-yellow-500 rounded h-1.5 transition-all" style="width: {{ number_format(($rec['detail']['pref'] ?? 0) * 100, 1) }}%"></div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center text-xs mt-2">
|
||||
<span class="text-gray-600">🎯 Cita-cita (5%)</span>
|
||||
<span class="font-semibold text-maroon">{{ number_format(($rec['detail']['cita'] ?? 0) * 100, 1) }}%</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded h-1.5">
|
||||
<div class="bg-blue-500 rounded h-1.5 transition-all" style="width: {{ number_format(($rec['detail']['cita'] ?? 0) * 100, 1) }}%"></div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center text-xs mt-2">
|
||||
<span class="text-gray-600">🏆 Prestasi (5%)</span>
|
||||
<span class="font-semibold text-maroon">{{ number_format(($rec['detail']['prestasi'] ?? 0) * 100, 1) }}%</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded h-1.5">
|
||||
<div class="bg-green-500 rounded h-1.5 transition-all" style="width: {{ number_format(($rec['detail']['prestasi'] ?? 0) * 100, 1) }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Explanation -->
|
||||
@if(isset($rec['explanation']) && is_array($rec['explanation']))
|
||||
<div class="bg-blue-50 p-3 rounded-lg border-l-4 border-blue-400">
|
||||
<p class="text-xs sm:text-sm font-bold text-blue-900 mb-2">💡 Alasan Cocok:</p>
|
||||
<ul class="space-y-1.5 text-xs sm:text-sm text-gray-800">
|
||||
<li class="flex gap-2">
|
||||
<span class="flex-shrink-0">📊</span>
|
||||
<span>{{ $rec['explanation']['nilai'] ?? '-' }}</span>
|
||||
</li>
|
||||
<li class="flex gap-2">
|
||||
<span class="flex-shrink-0">❤️</span>
|
||||
<span>{{ $rec['explanation']['minat'] ?? '-' }}</span>
|
||||
</li>
|
||||
<li class="flex gap-2">
|
||||
<span class="flex-shrink-0">🎓</span>
|
||||
<span>{{ $rec['explanation']['pref'] ?? '-' }}</span>
|
||||
</li>
|
||||
<li class="flex gap-2">
|
||||
<span class="flex-shrink-0">🎯</span>
|
||||
<span>{{ $rec['explanation']['cita'] ?? '-' }}</span>
|
||||
</li>
|
||||
<li class="flex gap-2">
|
||||
<span class="flex-shrink-0">🏆</span>
|
||||
<span>{{ $rec['explanation']['prestasi'] ?? '-' }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</details>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Chatbot Confirmation Card -->
|
||||
<div class="bg-white rounded-lg shadow-lg p-5 sm:p-8 mb-6 sm:mb-8 border-2 border-yellow-400">
|
||||
<div class="flex flex-col sm:flex-row items-start gap-3 sm:gap-4">
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ public function test_new_users_can_register(): void
|
|||
'email' => 'test@example.com',
|
||||
'password' => 'password',
|
||||
'password_confirmation' => 'password',
|
||||
'nis' => 'NIS123456',
|
||||
'kelompok_asal' => 'IPA',
|
||||
]);
|
||||
|
||||
$this->assertAuthenticated();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,116 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class CrudValidationTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_admin_can_add_jurusan_data(): void
|
||||
{
|
||||
$admin = User::factory()->create([
|
||||
'role' => 'admin',
|
||||
'email_verified_at' => now(),
|
||||
]);
|
||||
|
||||
$payload = [
|
||||
'nama_jurusan' => 'Jurusan Uji Admin',
|
||||
'deskripsi' => 'Deskripsi jurusan uji dari admin',
|
||||
'keywords' => 'uji,admin',
|
||||
'preferensi_studi' => 'Sains & Teknologi',
|
||||
'prospek_kerja' => 'Tester aplikasi',
|
||||
'bobot_mtk' => 0.8,
|
||||
'bobot_fisika' => 0.7,
|
||||
];
|
||||
|
||||
$response = $this->actingAs($admin)->post(route('admin.jurusan.store'), $payload);
|
||||
|
||||
$response->assertRedirect(route('admin.jurusan'));
|
||||
$this->assertDatabaseHas('polije_majors', [
|
||||
'nama_jurusan' => 'Jurusan Uji Admin',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_bk_can_add_jurusan_data(): void
|
||||
{
|
||||
$bk = User::factory()->create([
|
||||
'role' => 'bk',
|
||||
'email_verified_at' => now(),
|
||||
]);
|
||||
|
||||
$payload = [
|
||||
'nama_jurusan' => 'Jurusan Uji BK',
|
||||
'deskripsi' => 'Deskripsi jurusan uji dari BK',
|
||||
'keywords' => 'uji,bk',
|
||||
'preferensi_studi' => 'Bisnis & Manajemen',
|
||||
'prospek_kerja' => 'Konsultan BK',
|
||||
'bobot_ekonomi' => 0.9,
|
||||
'bobot_sosiologi' => 0.6,
|
||||
];
|
||||
|
||||
$response = $this->actingAs($bk)->post(route('bk.jurusan.store'), $payload);
|
||||
|
||||
$response->assertRedirect(route('bk.jurusan'));
|
||||
$this->assertDatabaseHas('polije_majors', [
|
||||
'nama_jurusan' => 'Jurusan Uji BK',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_admin_guru_bk_store_validates_email_and_password(): void
|
||||
{
|
||||
$admin = User::factory()->create([
|
||||
'role' => 'admin',
|
||||
'email_verified_at' => now(),
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($admin)->from(route('admin.guru-bk.create'))->post(route('admin.guru-bk.store'), [
|
||||
'name' => 'Guru BK Uji',
|
||||
'email' => 'email-tidak-valid',
|
||||
'password' => '123',
|
||||
'password_confirmation' => '123',
|
||||
]);
|
||||
|
||||
$response->assertRedirect(route('admin.guru-bk.create'));
|
||||
$response->assertSessionHasErrors(['email', 'password']);
|
||||
}
|
||||
|
||||
public function test_rekomendasi_ipa_requires_all_ipa_scores(): void
|
||||
{
|
||||
$siswa = User::factory()->create([
|
||||
'role' => 'siswa',
|
||||
'kelompok_asal' => 'IPA',
|
||||
'email_verified_at' => now(),
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($siswa)->from(route('rekomendasi.index'))->post(route('rekomendasi.proses'), [
|
||||
'mtk' => 90,
|
||||
'minat' => 'coding',
|
||||
'pref_studi' => 'Sains & Teknologi',
|
||||
'cita_cita' => 'programmer',
|
||||
]);
|
||||
|
||||
$response->assertRedirect(route('rekomendasi.index'));
|
||||
$response->assertSessionHasErrors(['fisika', 'kimia', 'biologi']);
|
||||
}
|
||||
|
||||
public function test_admin_student_detail_only_accepts_siswa_id(): void
|
||||
{
|
||||
$admin = User::factory()->create([
|
||||
'role' => 'admin',
|
||||
'email_verified_at' => now(),
|
||||
]);
|
||||
|
||||
$bk = User::factory()->create([
|
||||
'role' => 'bk',
|
||||
'email_verified_at' => now(),
|
||||
]);
|
||||
|
||||
$this->actingAs($admin)
|
||||
->get(route('admin.student.detail', $bk->id))
|
||||
->assertNotFound();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,252 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\PolijeMajor;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ExplainableRecommendationTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
// Create some jurusan data
|
||||
PolijeMajor::create([
|
||||
'nama_jurusan' => 'Teknologi Informasi',
|
||||
'singkatan' => 'TI',
|
||||
'tujuan_kompetensi' => 'Menghasilkan profesional IT',
|
||||
'prospek_kerja' => 'Software Developer, Software Engineer, IT Consultant',
|
||||
'kelompok_asal' => 'IPA',
|
||||
'mtk' => 30, 'fisika' => 20, 'kimia' => 10, 'biologi' => 5,
|
||||
'minat1' => 'Logika Komputer', 'minat2' => '', 'minat3' => '',
|
||||
'pref_sains' => 70, 'pref_pertanian' => 20, 'pref_kesehatan' => 10,
|
||||
'pref_bisnis' => 15, 'pref_sosial' => 10, 'pref_praktik' => 50, 'pref_teori' => 50,
|
||||
]);
|
||||
|
||||
PolijeMajor::create([
|
||||
'nama_jurusan' => 'Akuntansi',
|
||||
'singkatan' => 'AK',
|
||||
'tujuan_kompetensi' => 'Menghasilkan profesional akuntansi',
|
||||
'prospek_kerja' => 'Accountant, Financial Analyst, Tax Consultant',
|
||||
'kelompok_asal' => 'IPS',
|
||||
'ekonomi' => 30, 'geografi' => 15, 'sosiologi' => 10, 'sejarah' => 5,
|
||||
'minat1' => 'Bisnis', 'minat2' => '', 'minat3' => '',
|
||||
'pref_bisnis' => 80, 'pref_sosial' => 15, 'pref_pertanian' => 5,
|
||||
'pref_sains' => 10, 'pref_kesehatan' => 5, 'pref_praktik' => 60, 'pref_teori' => 40,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test bahwa response recommendation mencakup explanation
|
||||
*/
|
||||
public function test_recommendation_includes_explanation()
|
||||
{
|
||||
$user = User::factory()->create([
|
||||
'role' => 'siswa',
|
||||
'kelompok_asal' => 'IPA',
|
||||
]);
|
||||
|
||||
$this->actingAs($user)->post(route('rekomendasi.proses'), [
|
||||
'mtk' => 90,
|
||||
'fisika' => 85,
|
||||
'kimia' => 80,
|
||||
'biologi' => 75,
|
||||
'minat' => 'Logika Komputer',
|
||||
'pref_studi' => 'Sains & Teknologi',
|
||||
'cita_cita' => 'Software Developer',
|
||||
'prestasi' => 'Juara Kompetisi Coding Nasional',
|
||||
]);
|
||||
|
||||
// Verify recommendation is stored
|
||||
$lastRecommendation = \App\Models\Recommendation::latest()->first();
|
||||
$this->assertNotNull($lastRecommendation);
|
||||
|
||||
// Verify hasil_rekomendasi contains explanation
|
||||
$hasil = $lastRecommendation->hasil_rekomendasi;
|
||||
$this->assertIsArray($hasil);
|
||||
$this->assertNotEmpty($hasil);
|
||||
|
||||
// Check top recommendation has explanation field
|
||||
$topRec = $hasil[0];
|
||||
$this->assertArrayHasKey('explanation', $topRec);
|
||||
$this->assertIsArray($topRec['explanation']);
|
||||
|
||||
// Verify all 5 explanation keys exist
|
||||
$expectedKeys = ['nilai', 'minat', 'pref', 'cita', 'prestasi'];
|
||||
foreach ($expectedKeys as $key) {
|
||||
$this->assertArrayHasKey($key, $topRec['explanation']);
|
||||
$this->assertIsString($topRec['explanation'][$key]);
|
||||
$this->assertNotEmpty($topRec['explanation'][$key]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test bahwa explanation berisi teks yang meaningful
|
||||
*/
|
||||
public function test_explanation_contains_meaningful_text()
|
||||
{
|
||||
$user = User::factory()->create([
|
||||
'role' => 'siswa',
|
||||
'kelompok_asal' => 'IPA',
|
||||
]);
|
||||
|
||||
$this->actingAs($user)->post(route('rekomendasi.proses'), [
|
||||
'mtk' => 95,
|
||||
'fisika' => 90,
|
||||
'kimia' => 88,
|
||||
'biologi' => 92,
|
||||
'minat' => 'Logika Komputer',
|
||||
'pref_studi' => 'Sains & Teknologi',
|
||||
'cita_cita' => 'Software Engineer',
|
||||
'prestasi' => 'Juara Olimpiade Komputer',
|
||||
]);
|
||||
|
||||
$lastRecommendation = \App\Models\Recommendation::latest()->first();
|
||||
$hasil = $lastRecommendation->hasil_rekomendasi;
|
||||
$topRec = $hasil[0];
|
||||
$explanation = $topRec['explanation'];
|
||||
|
||||
// Verify explanation contains meaningful indicators and text
|
||||
$this->assertStringContainsString('Nilai akademik', $explanation['nilai']);
|
||||
$this->assertStringContainsString('Minat', $explanation['minat']);
|
||||
$this->assertStringContainsString('pembelajaran', $explanation['pref']);
|
||||
$this->assertStringContainsString('cita', $explanation['cita']);
|
||||
$this->assertStringContainsString('prestasi', strtolower($explanation['prestasi']));
|
||||
|
||||
// Verify checkmarks or indicators are present
|
||||
$combinedExplanation = implode(' ', $explanation);
|
||||
$this->assertTrue(
|
||||
str_contains($combinedExplanation, '✅') ||
|
||||
str_contains($combinedExplanation, '✓') ||
|
||||
str_contains($combinedExplanation, 'sesuai') ||
|
||||
str_contains($combinedExplanation, 'cocok')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test scoring detail (nilai, minat, pref, cita, prestasi) tersimpan dengan tepat
|
||||
*/
|
||||
public function test_scoring_detail_stored_correctly()
|
||||
{
|
||||
$user = User::factory()->create([
|
||||
'role' => 'siswa',
|
||||
'kelompok_asal' => 'IPA',
|
||||
]);
|
||||
|
||||
$this->actingAs($user)->post(route('rekomendasi.proses'), [
|
||||
'mtk' => 88,
|
||||
'fisika' => 82,
|
||||
'kimia' => 85,
|
||||
'biologi' => 80,
|
||||
'minat' => 'Logika Komputer dan Pemrograman',
|
||||
'pref_studi' => 'Sains & Teknologi',
|
||||
'cita_cita' => 'Software Engineer',
|
||||
'prestasi' => 'Sertifikat Oracle Java',
|
||||
]);
|
||||
|
||||
$lastRecommendation = \App\Models\Recommendation::latest()->first();
|
||||
$hasil = $lastRecommendation->hasil_rekomendasi;
|
||||
|
||||
// Verify detail breakdown exists for top recommendation
|
||||
$topRec = $hasil[0];
|
||||
$this->assertArrayHasKey('detail', $topRec);
|
||||
$detail = $topRec['detail'];
|
||||
|
||||
// Verify all 5 scoring components exist
|
||||
$scores = ['nilai', 'minat', 'pref', 'cita', 'prestasi'];
|
||||
foreach ($scores as $score) {
|
||||
$this->assertArrayHasKey($score, $detail);
|
||||
$this->assertIsNumeric($detail[$score]);
|
||||
$this->assertGreaterThanOrEqual(0, $detail[$score]);
|
||||
$this->assertLessThanOrEqual(1, $detail[$score]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test bahwa setiap jurusan dalam hasil punya explanation
|
||||
*/
|
||||
public function test_all_recommendations_have_explanations()
|
||||
{
|
||||
$user = User::factory()->create([
|
||||
'role' => 'siswa',
|
||||
'kelompok_asal' => 'IPA',
|
||||
]);
|
||||
|
||||
$this->actingAs($user)->post(route('rekomendasi.proses'), [
|
||||
'mtk' => 80,
|
||||
'fisika' => 75,
|
||||
'kimia' => 78,
|
||||
'biologi' => 76,
|
||||
'minat' => 'Sains dan Teknologi',
|
||||
'pref_studi' => 'Sains & Teknologi',
|
||||
'cita_cita' => 'Profesional Teknologi',
|
||||
'prestasi' => 'Aktif dalam kegiatan STEM',
|
||||
]);
|
||||
|
||||
$lastRecommendation = \App\Models\Recommendation::latest()->first();
|
||||
$hasil = $lastRecommendation->hasil_rekomendasi;
|
||||
|
||||
// Verify each recommendation has explanation and detail
|
||||
foreach ($hasil as $rec) {
|
||||
$this->assertArrayHasKey('explanation', $rec);
|
||||
$this->assertArrayHasKey('detail', $rec);
|
||||
$this->assertIsArray($rec['explanation']);
|
||||
$this->assertIsArray($rec['detail']);
|
||||
|
||||
// Count should match (5 criteria)
|
||||
$this->assertCount(5, $rec['explanation']);
|
||||
$this->assertCount(5, $rec['detail']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test explanation rendered in view
|
||||
*/
|
||||
public function test_explanation_displayed_in_view()
|
||||
{
|
||||
$user = User::factory()->create([
|
||||
'role' => 'siswa',
|
||||
'kelompok_asal' => 'IPA',
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)->post(route('rekomendasi.proses'), [
|
||||
'mtk' => 85,
|
||||
'fisika' => 80,
|
||||
'kimia' => 78,
|
||||
'biologi' => 82,
|
||||
'minat' => 'Logika Komputer',
|
||||
'pref_studi' => 'Sains & Teknologi',
|
||||
'cita_cita' => 'Software Developer',
|
||||
'prestasi' => 'Juara Informatika',
|
||||
]);
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
// View should contain explanation indicators
|
||||
$response->assertViewHas('hasilAkhir');
|
||||
$hasil = $response->viewData('hasilAkhir');
|
||||
|
||||
// Check explanation is in view data
|
||||
$this->assertNotEmpty($hasil);
|
||||
$topRec = $hasil[0];
|
||||
$this->assertArrayHasKey('explanation', $topRec);
|
||||
$explanation = $topRec['explanation'];
|
||||
|
||||
// Verify explanation content in response
|
||||
foreach ($explanation as $key => $text) {
|
||||
$this->assertNotEmpty($text);
|
||||
// Check that text contains expected keywords
|
||||
$hasContent = str_contains($text, '✅') ||
|
||||
str_contains($text, '✓') ||
|
||||
str_contains($text, 'sesuai') ||
|
||||
str_contains($text, 'cocok') ||
|
||||
str_contains($text, 'relevan');
|
||||
$this->assertTrue($hasContent, "Explanation for $key should have meaningful content");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -45,6 +45,6 @@ public function test_high_language_prefers_bahasa_komunikasi()
|
|||
|
||||
$response = $this->actingAs($user)->post(route('rekomendasi.proses'), $payload);
|
||||
$response->assertStatus(200);
|
||||
$response->assertSee('Bahasa, Komunikasi dan Pariwisata');
|
||||
$response->assertSee('Bahasa, Komunikasi, dan Pariwisata');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -113,6 +113,8 @@ private function mapMinat(string $minatRaw): string
|
|||
|
||||
private function scorePrestasiScore(string $prestasiRaw): float
|
||||
{
|
||||
$prestasiRaw = strtolower(trim($prestasiRaw));
|
||||
|
||||
if (empty($prestasiRaw)) {
|
||||
return 0.0;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue