Feat: UI Restructuring - Integrate history with main pages
- Combine recommendation form with history on single page - Combine chatbot interface with chat history on single page - Fix chatbot SQL error (DISTINCT + ORDER BY incompatibility) - Fix chatbot not showing old recommendations incorrectly - Remove history cards from dashboard (moved to respective pages) - Remove delete account from student profile (admin-only now) - All 49 PHPUnit tests passing
This commit is contained in:
parent
b48f27505e
commit
84b3fc4469
|
|
@ -66,27 +66,60 @@ public function index(Request $request)
|
||||||
// Tentukan recommendation_id:
|
// Tentukan recommendation_id:
|
||||||
// 1. Dari sesi lama (sudah diset di atas)
|
// 1. Dari sesi lama (sudah diset di atas)
|
||||||
// 2. Dari parameter ?rec= (klik dari hasil rekomendasi)
|
// 2. Dari parameter ?rec= (klik dari hasil rekomendasi)
|
||||||
// 3. Dari rekomendasi terbaru user
|
// JANGAN fallback ke rekomendasi terbaru - biarkan null jika tidak ada
|
||||||
if (!$recommendationId && $recId) {
|
if (!$recommendationId && $recId) {
|
||||||
$rec = Recommendation::where('id', $recId)
|
$rec = Recommendation::where('id', $recId)
|
||||||
->where('user_id', $user->id)
|
->where('user_id', $user->id)
|
||||||
->first();
|
->first();
|
||||||
$recommendationId = $rec ? $rec->id : null;
|
$recommendationId = $rec ? $rec->id : null;
|
||||||
}
|
}
|
||||||
|
// Jika tidak ada recommendation_id dari session atau ?rec param, biarkan null
|
||||||
if (!$recommendationId) {
|
|
||||||
$latestRec = Recommendation::where('user_id', $user->id)->latest()->first();
|
|
||||||
$recommendationId = $latestRec ? $latestRec->id : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ambil konteks rekomendasi berdasarkan ID spesifik
|
// Ambil konteks rekomendasi berdasarkan ID spesifik
|
||||||
$recentRecommendation = $this->getRecommendationContext($user, $recommendationId);
|
$recentRecommendation = $this->getRecommendationContext($user, $recommendationId) ?? [];
|
||||||
|
|
||||||
|
// Ambil 10 session terakhir yang unik untuk user
|
||||||
|
// Strategy: ambil last chat per session, sort by created_at, limit 10
|
||||||
|
$chatHistories = collect();
|
||||||
|
|
||||||
|
$sessions = ChatHistory::where('user_id', $user->id)
|
||||||
|
->select('id_sesi')
|
||||||
|
->distinct('id_sesi')
|
||||||
|
->get()
|
||||||
|
->pluck('id_sesi');
|
||||||
|
|
||||||
|
foreach ($sessions as $session_id) {
|
||||||
|
$lastChat = ChatHistory::where('user_id', $user->id)
|
||||||
|
->where('id_sesi', $session_id)
|
||||||
|
->latest('created_at')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$firstChat = ChatHistory::where('user_id', $user->id)
|
||||||
|
->where('id_sesi', $session_id)
|
||||||
|
->oldest('created_at')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($lastChat && $firstChat) {
|
||||||
|
$chatHistories->push((object)[
|
||||||
|
'id_sesi' => $session_id,
|
||||||
|
'created_at' => $lastChat->created_at,
|
||||||
|
'prompt' => $firstChat->prompt ?? 'Tidak ada pesan',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by created_at desc dan ambil 10
|
||||||
|
$chatHistories = $chatHistories->sortByDesc('created_at')
|
||||||
|
->take(10)
|
||||||
|
->values()
|
||||||
|
->toArray();
|
||||||
|
|
||||||
return view('chatbot.index', [
|
return view('chatbot.index', [
|
||||||
'recommendation' => $recentRecommendation,
|
'recommendation' => $recentRecommendation,
|
||||||
'sessionId' => $sessionId,
|
'sessionId' => $sessionId,
|
||||||
'previousMessages' => $previousMessages,
|
'previousMessages' => $previousMessages,
|
||||||
'recommendationId' => $recommendationId,
|
'recommendationId' => $recommendationId,
|
||||||
|
'chatHistories' => $chatHistories,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -192,14 +225,24 @@ public function historyChat()
|
||||||
$rec = $first->recommendation;
|
$rec = $first->recommendation;
|
||||||
$recInfo = null;
|
$recInfo = null;
|
||||||
if ($rec) {
|
if ($rec) {
|
||||||
$hasil = is_array($rec->hasil_rekomendasi)
|
// Safely decode hasil_rekomendasi
|
||||||
? $rec->hasil_rekomendasi
|
$hasil = [];
|
||||||
: json_decode($rec->hasil_rekomendasi, true);
|
if (!empty($rec->hasil_rekomendasi)) {
|
||||||
|
$hasil = is_array($rec->hasil_rekomendasi)
|
||||||
|
? $rec->hasil_rekomendasi
|
||||||
|
: json_decode($rec->hasil_rekomendasi, true);
|
||||||
|
|
||||||
|
// Validate hasil is array
|
||||||
|
if (!is_array($hasil)) {
|
||||||
|
$hasil = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$topJurusan = $hasil[0] ?? null;
|
$topJurusan = $hasil[0] ?? null;
|
||||||
$recInfo = [
|
$recInfo = [
|
||||||
'id' => $rec->id,
|
'id' => $rec->id,
|
||||||
'jurusan' => $topJurusan['jurusan'] ?? '-',
|
'jurusan' => is_array($topJurusan) ? ($topJurusan['jurusan'] ?? '-') : '-',
|
||||||
'skor' => $topJurusan['skor'] ?? 0,
|
'skor' => is_array($topJurusan) ? ($topJurusan['skor'] ?? 0) : 0,
|
||||||
'tanggal' => $rec->created_at,
|
'tanggal' => $rec->created_at,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
@ -232,45 +275,62 @@ private function getRecommendationContext($user, $recommendationId = null)
|
||||||
->first();
|
->first();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: dari session (saat baru selesai rekomendasi)
|
// ONLY fallback: dari session (saat baru selesai rekomendasi)
|
||||||
|
// Jangan ambil rekomendasi terbaru dari DB jika tidak ada recommendation_id
|
||||||
if (!$lastRec) {
|
if (!$lastRec) {
|
||||||
$sessionData = session('recomendation_data', null);
|
$sessionData = session('recomendation_data', null);
|
||||||
if ($sessionData) {
|
if ($sessionData) {
|
||||||
return $sessionData;
|
return $sessionData;
|
||||||
}
|
}
|
||||||
}
|
// Jika tidak ada di session dan tidak ada recommendation_id, return null
|
||||||
|
return null;
|
||||||
// Fallback: rekomendasi terbaru dari DB
|
|
||||||
if (!$lastRec) {
|
|
||||||
$lastRec = Recommendation::where('user_id', $user->id)
|
|
||||||
->latest()
|
|
||||||
->first();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$lastRec) {
|
if (!$lastRec) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$hasil = is_array($lastRec->hasil_rekomendasi)
|
// Safely decode hasil_rekomendasi
|
||||||
? $lastRec->hasil_rekomendasi
|
$hasil = [];
|
||||||
: json_decode($lastRec->hasil_rekomendasi, true);
|
if (!empty($lastRec->hasil_rekomendasi)) {
|
||||||
|
$hasil = is_array($lastRec->hasil_rekomendasi)
|
||||||
|
? $lastRec->hasil_rekomendasi
|
||||||
|
: json_decode($lastRec->hasil_rekomendasi, true);
|
||||||
|
|
||||||
|
// Validate hasil is array
|
||||||
|
if (!is_array($hasil)) {
|
||||||
|
$hasil = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$topJurusan = $hasil[0] ?? null;
|
$topJurusan = $hasil[0] ?? null;
|
||||||
$top3 = array_slice($hasil ?? [], 0, 3);
|
$top3 = array_slice($hasil ?? [], 0, 3);
|
||||||
|
|
||||||
// Hitung rata-rata dari kolom nilai
|
// Hitung rata-rata dari kolom nilai dengan safe access
|
||||||
$nilaiCols = ['mtk', 'fisika', 'kimia', 'biologi', 'ekonomi', 'geografi', 'sosiologi', 'sejarah'];
|
$nilaiCols = ['mtk', 'fisika', 'kimia', 'biologi', 'ekonomi', 'geografi', 'sosiologi', 'sejarah'];
|
||||||
$validVals = array_filter(array_map(fn($c) => $lastRec->$c, $nilaiCols), fn($v) => $v !== null);
|
$validVals = [];
|
||||||
|
foreach ($nilaiCols as $col) {
|
||||||
|
$val = $lastRec->getAttribute($col);
|
||||||
|
if ($val !== null && is_numeric($val)) {
|
||||||
|
$validVals[] = $val;
|
||||||
|
}
|
||||||
|
}
|
||||||
$rataRata = count($validVals) > 0 ? round(array_sum($validVals) / count($validVals), 1) : null;
|
$rataRata = count($validVals) > 0 ? round(array_sum($validVals) / count($validVals), 1) : null;
|
||||||
|
|
||||||
// Kategorisasi
|
// Kategorisasi nilai
|
||||||
$katNilai = 'Rendah';
|
$katNilai = 'Rendah';
|
||||||
if ($rataRata >= 85) $katNilai = 'Tinggi';
|
if ($rataRata !== null) {
|
||||||
elseif ($rataRata >= 70) $katNilai = 'Sedang';
|
if ($rataRata >= 85) {
|
||||||
|
$katNilai = 'Tinggi';
|
||||||
|
} elseif ($rataRata >= 70) {
|
||||||
|
$katNilai = 'Sedang';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'jurusan' => $topJurusan['jurusan'] ?? null,
|
'jurusan' => $topJurusan['jurusan'] ?? null,
|
||||||
'skor' => $topJurusan['skor'] ?? null,
|
'skor' => $topJurusan['skor'] ?? null,
|
||||||
'detail' => $topJurusan['detail'] ?? [],
|
'detail' => is_array($topJurusan['detail'] ?? null) ? $topJurusan['detail'] : [],
|
||||||
'nilai' => $katNilai,
|
'nilai' => $katNilai,
|
||||||
'rata_rata' => $rataRata,
|
'rata_rata' => $rataRata,
|
||||||
'minat' => $lastRec->minat,
|
'minat' => $lastRec->minat,
|
||||||
|
|
@ -278,8 +338,8 @@ private function getRecommendationContext($user, $recommendationId = null)
|
||||||
'cita_cita' => $lastRec->cita_cita,
|
'cita_cita' => $lastRec->cita_cita,
|
||||||
'prestasi' => $lastRec->prestasi,
|
'prestasi' => $lastRec->prestasi,
|
||||||
'top3' => array_map(fn($r) => [
|
'top3' => array_map(fn($r) => [
|
||||||
'jurusan' => $r['jurusan'] ?? '',
|
'jurusan' => is_array($r) ? ($r['jurusan'] ?? '') : '',
|
||||||
'skor' => $r['skor'] ?? 0,
|
'skor' => is_array($r) ? ($r['skor'] ?? 0) : 0,
|
||||||
], $top3),
|
], $top3),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,38 +25,44 @@ public function index()
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return view('rekomendasi.input', compact('student'));
|
// Ambil riwayat rekomendasi user (limit 10 terakhir, diurutkan dari terbaru)
|
||||||
|
$recommendations = Recommendation::where('user_id', $user->id)
|
||||||
|
->orderBy('created_at', 'desc')
|
||||||
|
->limit(10)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return view('rekomendasi.input', compact('student', 'recommendations'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate textual explanation untuk setiap kriteria
|
* Generate textual explanation untuk setiap kriteria
|
||||||
* Menjelaskan "mengapa jurusan ini cocok" berdasarkan scoring detail
|
* Menjelaskan "mengapa jurusan ini cocok" berdasarkan scoring detail
|
||||||
*/
|
*/
|
||||||
private function generateExplanation($jurusanNama, $detail, $katNilai, $kategoriMinat, $prefStudi, $prestasi, array $prestasiAnalysis = [])
|
private function generateExplanation($jurusanNama, $detail, $katNilai, $kategoriMinat, $prefStudi, $prestasiRaw, array $prestasiAnalysis = [])
|
||||||
{
|
{
|
||||||
$explanations = [];
|
$explanations = [];
|
||||||
|
|
||||||
// 1. Penjelasan Nilai Akademik
|
// 1. Penjelasan Nilai Akademik (Kriteria 1)
|
||||||
$skorNilai = $detail['nilai'] ?? 0;
|
$skorNilai = $detail['nilai'] ?? 0;
|
||||||
if ($skorNilai >= 0.8) {
|
if ($skorNilai >= 0.8) {
|
||||||
$explanations['nilai'] = "✅ Nilai akademik Anda ($katNilai) sangat sesuai dengan jalur pendidikan yang dibutuhkan jurusan ini.";
|
$explanations['nilai'] = "✅ Nilai akademik Anda ($katNilai: avg score tinggi) sangat sesuai dengan jalur pendidikan yang dibutuhkan jurusan ini.";
|
||||||
} elseif ($skorNilai >= 0.6) {
|
} elseif ($skorNilai >= 0.6) {
|
||||||
$explanations['nilai'] = "✓ Nilai akademik Anda ($katNilai) cukup sesuai dengan persyaratan jurusan ini.";
|
$explanations['nilai'] = "✓ Nilai akademik Anda ($katNilai) cukup sesuai dengan persyaratan jurusan ini.";
|
||||||
} else {
|
} else {
|
||||||
$explanations['nilai'] = "⚠️ Nilai akademik Anda ($katNilai) masih perlu ditingkatkan untuk optimal di jurusan ini, namun tetap relevan.";
|
$explanations['nilai'] = "⚠️ Nilai akademik Anda ($katNilai) masih perlu ditingkatkan untuk optimal di jurusan ini, namun tetap relevan.";
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Penjelasan Minat
|
// 2. Penjelasan Minat (Kriteria 2)
|
||||||
$skorMinat = $detail['minat'] ?? 0;
|
$skorMinat = $detail['minat'] ?? 0;
|
||||||
if ($skorMinat >= 0.8) {
|
if ($skorMinat >= 0.8) {
|
||||||
$explanations['minat'] = "✅ Minat Anda sangat sesuai dan cocok dengan fokus kurikulum $jurusanNama.";
|
$explanations['minat'] = "✅ Minat Anda ($kategoriMinat) sangat sesuai dan cocok dengan fokus kurikulum $jurusanNama. Anda akan mempelajari hal-hal yang Anda sukai.";
|
||||||
} elseif ($skorMinat >= 0.6) {
|
} elseif ($skorMinat >= 0.6) {
|
||||||
$explanations['minat'] = "✓ Minat Anda cukup relevan dan sesuai dengan area pembelajaran di $jurusanNama.";
|
$explanations['minat'] = "✓ Minat Anda ($kategoriMinat) cukup relevan dan sesuai dengan area pembelajaran di $jurusanNama.";
|
||||||
} else {
|
} else {
|
||||||
$explanations['minat'] = "ℹ️ Minat Anda memiliki kesamaan dan relevansi dengan aspek-aspek tertentu di $jurusanNama.";
|
$explanations['minat'] = "ℹ️ Minat Anda ($kategoriMinat) memiliki kesamaan dan relevansi dengan aspek-aspek tertentu di $jurusanNama.";
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Penjelasan Preferensi Studi
|
// 3. Penjelasan Preferensi Studi (Kriteria 3)
|
||||||
$skorPref = $detail['pref'] ?? 0;
|
$skorPref = $detail['pref'] ?? 0;
|
||||||
if ($skorPref >= 0.8) {
|
if ($skorPref >= 0.8) {
|
||||||
$explanations['pref'] = "✅ Metode pembelajaran \"$prefStudi\" yang Anda pilih sangat sesuai dengan pendekatan pembelajaran $jurusanNama.";
|
$explanations['pref'] = "✅ Metode pembelajaran \"$prefStudi\" yang Anda pilih sangat sesuai dengan pendekatan pembelajaran $jurusanNama.";
|
||||||
|
|
@ -66,39 +72,41 @@ private function generateExplanation($jurusanNama, $detail, $katNilai, $kategori
|
||||||
$explanations['pref'] = "ℹ️ Jurusan ini menawarkan elemen pembelajaran \"$prefStudi\" yang relevan dengan preferensi Anda.";
|
$explanations['pref'] = "ℹ️ Jurusan ini menawarkan elemen pembelajaran \"$prefStudi\" yang relevan dengan preferensi Anda.";
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Penjelasan Cita-cita
|
// 4. Penjelasan Cita-cita (Kriteria 4) - IMPROVED with more detail
|
||||||
$skorCita = $detail['cita'] ?? 0;
|
$skorCita = $detail['cita'] ?? 0;
|
||||||
if ($skorCita >= 0.8) {
|
if ($skorCita >= 0.8) {
|
||||||
$explanations['cita'] = "✅ Cita-cita karir Anda sangat sesuai dan aligned dengan standar lulusan bidang ini.";
|
$explanations['cita'] = "✅ Cita-cita karir Anda sangat sesuai dan aligned dengan standar lulusan $jurusanNama. Jurusan ini secara langsung mempersiapkan Anda untuk mencapai cita-cita tersebut.";
|
||||||
} elseif ($skorCita >= 0.6) {
|
} elseif ($skorCita >= 0.6) {
|
||||||
$explanations['cita'] = "✓ Cita-cita Anda memiliki potensi besar untuk dicapai melalui jurusan ini.";
|
$explanations['cita'] = "✓ Cita-cita Anda memiliki potensi besar untuk dicapai melalui pendidikan di $jurusanNama. Kurikulum akan membekali skills yang relevan.";
|
||||||
} else {
|
} else {
|
||||||
$explanations['cita'] = "ℹ️ Jurusan ini membuka jalur karir yang sesuai dengan cita-cita dan aspirasi Anda.";
|
$explanations['cita'] = "ℹ️ Jurusan ini membuka jalur karir yang sesuai dengan cita-cita dan aspirasi Anda, meski tidak secara langsung target-nya.";
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Penjelasan Prestasi
|
// 5. Penjelasan Prestasi (Kriteria 5) - IMPROVED with more detail
|
||||||
$skorPrestasi = $detail['prestasi'] ?? 0;
|
$skorPrestasi = $detail['prestasi'] ?? 0;
|
||||||
if (!($prestasiAnalysis['provided'] ?? false)) {
|
if (!($prestasiAnalysis['provided'] ?? false)) {
|
||||||
$explanations['prestasi'] = "ℹ️ Prestasi tidak diisi, sehingga atribut prestasi tidak dihitung pada proses skoring.";
|
$explanations['prestasi'] = "ℹ️ Prestasi tidak diisi. Jika Anda memiliki prestasi atau achievement, itu dapat meningkatkan score untuk jurusan ini.";
|
||||||
return $explanations;
|
return $explanations;
|
||||||
}
|
}
|
||||||
|
|
||||||
$levelPrestasi = $prestasiAnalysis['level'] ?? 'minimal';
|
$levelPrestasi = $prestasiAnalysis['level'] ?? 'minimal';
|
||||||
|
$rawPrestasi = $prestasiAnalysis['raw'] ?? '';
|
||||||
|
|
||||||
$labelLevel = [
|
$labelLevel = [
|
||||||
'tinggi' => 'tinggi',
|
'tinggi' => 'TINGGI (Juara/Winner)',
|
||||||
'sedang' => 'menengah',
|
'sedang' => 'MENENGAH (Finalis/Medalist)',
|
||||||
'cukup' => 'dasar',
|
'cukup' => 'DASAR (Peserta/Sertifikat)',
|
||||||
'minimal' => 'minimal',
|
'minimal' => 'MINIMAL',
|
||||||
][$levelPrestasi] ?? 'minimal';
|
];
|
||||||
|
|
||||||
if ($skorPrestasi >= 0.7) {
|
if ($skorPrestasi >= 0.8) {
|
||||||
$explanations['prestasi'] = "✅ Prestasi Anda terdeteksi pada level {$labelLevel} dan memberi kontribusi kuat untuk kecocokan jurusan ini.";
|
$explanations['prestasi'] = "✅ Prestasi Anda ($labelLevel[$levelPrestasi]): \"$rawPrestasi\" sangat relevan dengan $jurusanNama. Ini menunjukkan Anda memiliki dedication dan capability.";
|
||||||
} elseif ($skorPrestasi >= 0.4) {
|
} elseif ($skorPrestasi >= 0.6) {
|
||||||
$explanations['prestasi'] = "✓ Prestasi Anda berada pada level {$labelLevel} dan tetap dipertimbangkan sebagai faktor pendukung.";
|
$explanations['prestasi'] = "✓ Prestasi Anda ($labelLevel[$levelPrestasi]): \"$rawPrestasi\" cukup relevan dan menunjukkan potensi di bidang ini.";
|
||||||
} else {
|
} else {
|
||||||
$explanations['prestasi'] = "ℹ️ Input prestasi tetap dihitung (level {$labelLevel}), namun saat ini kontribusinya relatif kecil dibanding faktor utama lain.";
|
$explanations['prestasi'] = "ℹ️ Prestasi Anda ($labelLevel[$levelPrestasi]): \"$rawPrestasi\" menunjukkan usaha yang dapat dikembangkan lebih lanjut di $jurusanNama.";
|
||||||
}
|
}
|
||||||
|
|
||||||
return $explanations;
|
return $explanations;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -109,6 +117,7 @@ public function proses(Request $request)
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
$kelompokAsal = strtoupper($user->kelompok_asal ?? 'IPA');
|
$kelompokAsal = strtoupper($user->kelompok_asal ?? 'IPA');
|
||||||
|
|
||||||
|
// Enhanced validation rules dengan lebih strict untuk non-akademik fields
|
||||||
$rules = [
|
$rules = [
|
||||||
'mtk' => 'nullable|numeric|min:0|max:100',
|
'mtk' => 'nullable|numeric|min:0|max:100',
|
||||||
'fisika' => 'nullable|numeric|min:0|max:100',
|
'fisika' => 'nullable|numeric|min:0|max:100',
|
||||||
|
|
@ -118,10 +127,10 @@ public function proses(Request $request)
|
||||||
'geografi' => 'nullable|numeric|min:0|max:100',
|
'geografi' => 'nullable|numeric|min:0|max:100',
|
||||||
'sosiologi' => 'nullable|numeric|min:0|max:100',
|
'sosiologi' => 'nullable|numeric|min:0|max:100',
|
||||||
'sejarah' => 'nullable|numeric|min:0|max:100',
|
'sejarah' => 'nullable|numeric|min:0|max:100',
|
||||||
'minat' => 'required|string|max:255',
|
'minat' => 'required|string|min:3|max:255',
|
||||||
'pref_studi' => 'required|string',
|
'pref_studi' => 'required|string|in:Sains & Teknologi,Pertanian & Lingkungan,Kesehatan & Ilmu Hayat,Bisnis & Manajemen,Sosial & Humaniora',
|
||||||
'cita_cita' => 'required|string|max:255',
|
'cita_cita' => 'required|string|min:3|max:255',
|
||||||
'prestasi' => 'nullable|string|max:255',
|
'prestasi' => 'nullable|string|min:3|max:255',
|
||||||
];
|
];
|
||||||
|
|
||||||
if ($kelompokAsal === 'IPA') {
|
if ($kelompokAsal === 'IPA') {
|
||||||
|
|
@ -136,7 +145,18 @@ public function proses(Request $request)
|
||||||
$rules['sejarah'] = 'required|numeric|min:0|max:100';
|
$rules['sejarah'] = 'required|numeric|min:0|max:100';
|
||||||
}
|
}
|
||||||
|
|
||||||
$validated = $request->validate($rules);
|
// Custom error messages untuk lebih informatif
|
||||||
|
$messages = [
|
||||||
|
'minat.required' => 'Minat harus diisi (minimal 3 karakter)',
|
||||||
|
'minat.min' => 'Minat terlalu pendek, jelaskan lebih detail',
|
||||||
|
'cita_cita.required' => 'Cita-cita harus diisi (minimal 3 karakter)',
|
||||||
|
'cita_cita.min' => 'Cita-cita terlalu pendek, jelaskan lebih detail',
|
||||||
|
'prestasi.min' => 'Prestasi terlalu pendek, jelaskan lebih detail',
|
||||||
|
'pref_studi.required' => 'Pilih salah satu preferensi studi',
|
||||||
|
'pref_studi.in' => 'Preferensi studi tidak valid',
|
||||||
|
];
|
||||||
|
|
||||||
|
$validated = $request->validate($rules, $messages);
|
||||||
|
|
||||||
// --- 1. PREPROCESSING NILAI (Kriteria 1: Akademik) ---
|
// --- 1. PREPROCESSING NILAI (Kriteria 1: Akademik) ---
|
||||||
$scores = $request->only(['mtk', 'fisika', 'kimia', 'biologi', 'ekonomi', 'geografi', 'sosiologi', 'sejarah']);
|
$scores = $request->only(['mtk', 'fisika', 'kimia', 'biologi', 'ekonomi', 'geografi', 'sosiologi', 'sejarah']);
|
||||||
|
|
@ -155,8 +175,24 @@ public function proses(Request $request)
|
||||||
|
|
||||||
// --- 2. ANALISIS MINAT (Kriteria 2) ---
|
// --- 2. ANALISIS MINAT (Kriteria 2) ---
|
||||||
$minatInput = trim((string) ($validated['minat'] ?? ''));
|
$minatInput = trim((string) ($validated['minat'] ?? ''));
|
||||||
|
|
||||||
|
// Validasi minat tidak hanya satu kata
|
||||||
|
if (strlen($minatInput) < 3) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Minat harus diisi dengan minimal 3 karakter untuk analisis yang akurat',
|
||||||
|
])->setStatusCode(422);
|
||||||
|
}
|
||||||
|
|
||||||
$minatRaw = strtolower($minatInput);
|
$minatRaw = strtolower($minatInput);
|
||||||
$minatMapped = $this->mapMinat($minatRaw);
|
$minatMapped = $this->mapMinat($minatRaw);
|
||||||
|
|
||||||
|
// Log untuk audit trail
|
||||||
|
\Log::debug('Minat Analysis', [
|
||||||
|
'input' => $minatInput,
|
||||||
|
'normalized' => $minatRaw,
|
||||||
|
'mapped' => $minatMapped,
|
||||||
|
]);
|
||||||
|
|
||||||
// --- 3. PREFERENSI STUDI LANJUTAN (Kriteria 3) ---
|
// --- 3. PREFERENSI STUDI LANJUTAN (Kriteria 3) ---
|
||||||
$prefStudi = $validated['pref_studi'];
|
$prefStudi = $validated['pref_studi'];
|
||||||
|
|
@ -164,15 +200,40 @@ public function proses(Request $request)
|
||||||
|
|
||||||
// --- 4. ANALISIS CITA-CITA (Kriteria 4) ---
|
// --- 4. ANALISIS CITA-CITA (Kriteria 4) ---
|
||||||
$citaInput = trim((string) ($validated['cita_cita'] ?? ''));
|
$citaInput = trim((string) ($validated['cita_cita'] ?? ''));
|
||||||
|
|
||||||
|
// Validasi cita-cita tidak hanya satu kata
|
||||||
|
if (strlen($citaInput) < 3) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Cita-cita harus diisi dengan minimal 3 karakter untuk analisis yang akurat',
|
||||||
|
])->setStatusCode(422);
|
||||||
|
}
|
||||||
|
|
||||||
$citaRaw = strtolower($citaInput);
|
$citaRaw = strtolower($citaInput);
|
||||||
$citaMapped = $this->mapCitaCita($citaRaw);
|
$citaMapped = $this->mapCitaCita($citaRaw);
|
||||||
|
|
||||||
|
// Log untuk audit trail
|
||||||
|
\Log::debug('Cita-cita Analysis', [
|
||||||
|
'input' => $citaInput,
|
||||||
|
'normalized' => $citaRaw,
|
||||||
|
'mapped' => $citaMapped,
|
||||||
|
]);
|
||||||
|
|
||||||
// --- 5. ANALISIS PRESTASI (Kriteria 5) ---
|
// --- 5. ANALISIS PRESTASI (Kriteria 5) ---
|
||||||
$prestasiInput = trim((string) ($validated['prestasi'] ?? ''));
|
$prestasiInput = trim((string) ($validated['prestasi'] ?? ''));
|
||||||
$isPrestasiFilled = $prestasiInput !== '';
|
$isPrestasiFilled = $prestasiInput !== '' && strlen($prestasiInput) >= 3;
|
||||||
$prestasiRaw = strtolower($prestasiInput);
|
$prestasiRaw = strtolower($prestasiInput);
|
||||||
$prestasiAnalysis = $this->analyzePrestasi($prestasiRaw);
|
$prestasiAnalysis = $this->analyzePrestasi($prestasiRaw);
|
||||||
$prestasiScore = $prestasiAnalysis['score'];
|
$prestasiScore = $prestasiAnalysis['score'];
|
||||||
|
|
||||||
|
// Log untuk audit trail
|
||||||
|
\Log::debug('Prestasi Analysis', [
|
||||||
|
'input' => $prestasiInput,
|
||||||
|
'is_filled' => $isPrestasiFilled,
|
||||||
|
'normalized' => $prestasiRaw,
|
||||||
|
'level' => $prestasiAnalysis['level'] ?? 'not provided',
|
||||||
|
'score' => $prestasiScore,
|
||||||
|
]);
|
||||||
|
|
||||||
// --- 6. PERHITUNGAN NAIVE BAYES BERBOBOT ---
|
// --- 6. PERHITUNGAN NAIVE BAYES BERBOBOT ---
|
||||||
$cfg = config('polije.criteria', []);
|
$cfg = config('polije.criteria', []);
|
||||||
|
|
@ -181,35 +242,63 @@ public function proses(Request $request)
|
||||||
$detailPerJurusan = [];
|
$detailPerJurusan = [];
|
||||||
$epsilon = 1e-9;
|
$epsilon = 1e-9;
|
||||||
|
|
||||||
|
// Validate config exists
|
||||||
|
if (empty($cfg)) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Konfigurasi sistem rekomendasi tidak ditemukan',
|
||||||
|
])->setStatusCode(500);
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($cfg as $jurusan => $c) {
|
foreach ($cfg as $jurusan => $c) {
|
||||||
// Prior: uniform
|
// Prior: uniform dengan safety check
|
||||||
$prior = 1 / count($cfg);
|
$cfgCount = max(1, count($cfg)); // Prevent division by zero
|
||||||
|
$prior = 1 / $cfgCount;
|
||||||
$logPrior = log(max($prior, $epsilon));
|
$logPrior = log(max($prior, $epsilon));
|
||||||
|
|
||||||
// Weights dan match probabilities
|
// Weights dan match probabilities dengan defaults
|
||||||
$weights = $c['weights'] ?? ['nilai' => 0.40, 'minat' => 0.35, 'pref' => 0.15, 'prestasi' => 0.05, 'cita_cita' => 0.05];
|
$weights = $c['weights'] ?? ['nilai' => 0.40, 'minat' => 0.35, 'pref' => 0.15, 'prestasi' => 0.05, 'cita_cita' => 0.05];
|
||||||
|
|
||||||
|
// Ensure weights is array
|
||||||
|
if (!is_array($weights)) {
|
||||||
|
$weights = ['nilai' => 0.40, 'minat' => 0.35, 'pref' => 0.15, 'prestasi' => 0.05, 'cita_cita' => 0.05];
|
||||||
|
}
|
||||||
|
|
||||||
// Jika prestasi kosong, atribut prestasi tidak dihitung.
|
// Jika prestasi kosong, atribut prestasi tidak dihitung dengan normalisasi ulang
|
||||||
if (!$isPrestasiFilled) {
|
if (!$isPrestasiFilled) {
|
||||||
$weights['prestasi'] = 0.0;
|
$weights['prestasi'] = 0.0;
|
||||||
$sumNonPrestasi = ($weights['nilai'] ?? 0) + ($weights['minat'] ?? 0) + ($weights['pref'] ?? 0) + ($weights['cita_cita'] ?? 0);
|
$sumNonPrestasi = ($weights['nilai'] ?? 0) + ($weights['minat'] ?? 0) + ($weights['pref'] ?? 0) + ($weights['cita_cita'] ?? 0);
|
||||||
if ($sumNonPrestasi > 0) {
|
|
||||||
|
// Normalize weights dengan safety check
|
||||||
|
if ($sumNonPrestasi > $epsilon) {
|
||||||
$weights['nilai'] = ($weights['nilai'] ?? 0) / $sumNonPrestasi;
|
$weights['nilai'] = ($weights['nilai'] ?? 0) / $sumNonPrestasi;
|
||||||
$weights['minat'] = ($weights['minat'] ?? 0) / $sumNonPrestasi;
|
$weights['minat'] = ($weights['minat'] ?? 0) / $sumNonPrestasi;
|
||||||
$weights['pref'] = ($weights['pref'] ?? 0) / $sumNonPrestasi;
|
$weights['pref'] = ($weights['pref'] ?? 0) / $sumNonPrestasi;
|
||||||
$weights['cita_cita'] = ($weights['cita_cita'] ?? 0) / $sumNonPrestasi;
|
$weights['cita_cita'] = ($weights['cita_cita'] ?? 0) / $sumNonPrestasi;
|
||||||
|
} else {
|
||||||
|
// Fallback weights jika semua weight adalah 0
|
||||||
|
$weights = ['nilai' => 0.4, 'minat' => 0.35, 'pref' => 0.15, 'cita_cita' => 0.1];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$matchProb = $c['match_prob'] ?? ['nilai' => 0.80, 'minat' => 0.90, 'pref' => 0.85, 'prestasi' => 0.65, 'cita_cita' => 0.85];
|
$matchProb = $c['match_prob'] ?? ['nilai' => 0.80, 'minat' => 0.90, 'pref' => 0.85, 'prestasi' => 0.65, 'cita_cita' => 0.85];
|
||||||
|
|
||||||
|
// Ensure matchProb is array
|
||||||
|
if (!is_array($matchProb)) {
|
||||||
|
$matchProb = ['nilai' => 0.80, 'minat' => 0.90, 'pref' => 0.85, 'prestasi' => 0.65, 'cita_cita' => 0.85];
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Likelihood untuk Nilai
|
// 1. Likelihood untuk Nilai
|
||||||
// Tetap berbasis atribut Nilai Akademik, namun dibuat lebih granular:
|
|
||||||
// kombinasi kategori nilai + kecocokan bobot mapel per jurusan (dari data PolijeMajor).
|
|
||||||
$p_nilai_category = ($katNilai == ($c['nilai'] ?? 'Sedang'))
|
$p_nilai_category = ($katNilai == ($c['nilai'] ?? 'Sedang'))
|
||||||
? $matchProb['nilai']
|
? ($matchProb['nilai'] ?? 0.80)
|
||||||
: max(1 - $matchProb['nilai'], $epsilon);
|
: max(1 - ($matchProb['nilai'] ?? 0.80), $epsilon);
|
||||||
|
|
||||||
|
// Safe access to majorMap with null check
|
||||||
|
$majorRecord = $majorMap[$jurusan] ?? null;
|
||||||
|
$bobotMapel = $majorRecord ? ($majorRecord->bobot_mapel ?? []) : [];
|
||||||
|
|
||||||
$p_nilai_subject = $this->scoreSubjectFitLikelihood(
|
$p_nilai_subject = $this->scoreSubjectFitLikelihood(
|
||||||
$majorMap[$jurusan]->bobot_mapel ?? [],
|
$bobotMapel,
|
||||||
$scores,
|
$scores,
|
||||||
$p_nilai_category
|
$p_nilai_category
|
||||||
);
|
);
|
||||||
|
|
@ -220,7 +309,7 @@ public function proses(Request $request)
|
||||||
$minatRaw,
|
$minatRaw,
|
||||||
$minatMapped,
|
$minatMapped,
|
||||||
$c['minat'] ?? 'Umum',
|
$c['minat'] ?? 'Umum',
|
||||||
$matchProb['minat']
|
$matchProb['minat'] ?? 0.90
|
||||||
);
|
);
|
||||||
|
|
||||||
// 3. Likelihood untuk Preferensi Studi
|
// 3. Likelihood untuk Preferensi Studi
|
||||||
|
|
@ -228,17 +317,17 @@ public function proses(Request $request)
|
||||||
if (!is_array($prefList)) {
|
if (!is_array($prefList)) {
|
||||||
$prefList = [$prefList];
|
$prefList = [$prefList];
|
||||||
}
|
}
|
||||||
$p_pref = in_array($prefStudi, $prefList) ? $matchProb['pref'] : max(1 - $matchProb['pref'], $epsilon);
|
$p_pref = in_array($prefStudi, $prefList) ? ($matchProb['pref'] ?? 0.85) : max(1 - ($matchProb['pref'] ?? 0.85), $epsilon);
|
||||||
|
|
||||||
// 4. Likelihood untuk Cita-cita
|
// 4. Likelihood untuk Cita-cita
|
||||||
$citaCitaKeywords = $c['cita_cita_keywords'] ?? [];
|
$citaCitaKeywords = $c['cita_cita_keywords'] ?? [];
|
||||||
$p_cita_cita = $this->scoreKeywordLikelihood($citaMapped, $citaCitaKeywords, $matchProb['cita_cita']);
|
$p_cita_cita = $this->scoreKeywordLikelihood($citaMapped, $citaCitaKeywords, $matchProb['cita_cita'] ?? 0.85);
|
||||||
|
|
||||||
// 5. Likelihood untuk Prestasi (bertingkat: tinggi/menengah/dasar/minimal)
|
// 5. Likelihood untuk Prestasi (bertingkat: tinggi/menengah/dasar/minimal)
|
||||||
$p_prestasi = $this->scorePrestasiLikelihood(
|
$p_prestasi = $this->scorePrestasiLikelihood(
|
||||||
$prestasiAnalysis,
|
$prestasiAnalysis,
|
||||||
$citaCitaKeywords,
|
$citaCitaKeywords,
|
||||||
$matchProb['prestasi']
|
$matchProb['prestasi'] ?? 0.65
|
||||||
);
|
);
|
||||||
|
|
||||||
// Simpan detail per kriteria untuk tampilan
|
// Simpan detail per kriteria untuk tampilan
|
||||||
|
|
@ -348,29 +437,115 @@ public function proses(Request $request)
|
||||||
/**
|
/**
|
||||||
* Pemetaan minat ke kategori yang dipahami sistem
|
* Pemetaan minat ke kategori yang dipahami sistem
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Normalize text untuk better keyword matching
|
||||||
|
* Menangani variasi kata seperti programmer/programming, coding/code
|
||||||
|
*/
|
||||||
|
private function normalizeText(string $text): string
|
||||||
|
{
|
||||||
|
$text = strtolower(trim($text));
|
||||||
|
|
||||||
|
// Simple stemming untuk common variations
|
||||||
|
$replacements = [
|
||||||
|
'programmer' => 'programming',
|
||||||
|
'coder' => 'coding',
|
||||||
|
'code' => 'coding',
|
||||||
|
'codes' => 'coding',
|
||||||
|
'develop' => 'development',
|
||||||
|
'developer' => 'development',
|
||||||
|
'develops' => 'development',
|
||||||
|
'manager' => 'manajemen',
|
||||||
|
'manages' => 'manajemen',
|
||||||
|
'doctor' => 'dokter',
|
||||||
|
'doctors' => 'dokter',
|
||||||
|
'nurse' => 'perawat',
|
||||||
|
'nurses' => 'perawat',
|
||||||
|
'engineer' => 'teknik',
|
||||||
|
'engineers' => 'teknik',
|
||||||
|
'farming' => 'pertanian',
|
||||||
|
'farmer' => 'pertanian',
|
||||||
|
'farmers' => 'pertanian',
|
||||||
|
'business' => 'bisnis',
|
||||||
|
'businessmen' => 'bisnis',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($replacements as $from => $to) {
|
||||||
|
$text = str_replace($from, $to, $text);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $text;
|
||||||
|
}
|
||||||
|
|
||||||
private function mapMinat(string $minatRaw): string
|
private function mapMinat(string $minatRaw): string
|
||||||
{
|
{
|
||||||
if (preg_match('/(coding|komputer|laptop|web|aplikasi|logika|programming|software|development)/', $minatRaw)) {
|
// Normalize text untuk better matching
|
||||||
return 'Logika & Komputer';
|
$minatNormalized = $this->normalizeText($minatRaw);
|
||||||
} elseif (preg_match('/(tanam|kebun|sawah|hewan|ternak|alam|pertanian|agri)/', $minatRaw)) {
|
|
||||||
return 'Alam & Tanaman';
|
// Use coverage-based scoring untuk handle ambiguous inputs
|
||||||
} elseif (preg_match('/(obat|sakit|rawat|medis|gizi|sehat|kesehatan|perawat|dokter)/', $minatRaw)) {
|
$categoryKeywords = [
|
||||||
return 'Pelayanan & Kesehatan';
|
'Logika & Komputer' => ['coding', 'komputer', 'laptop', 'web', 'aplikasi', 'logika', 'programming', 'software', 'development', 'developer', 'it', 'data', 'ai'],
|
||||||
} elseif (preg_match('/(bisnis|uang|jual|kantor|hitung|ekonomi|dagang|usaha|entrepreneur)/', $minatRaw)) {
|
'Alam & Tanaman' => ['tanam', 'kebun', 'sawah', 'hewan', 'ternak', 'alam', 'pertanian', 'agri', 'panen', 'tani', 'hortikultura'],
|
||||||
return 'Manajemen & Bisnis';
|
'Pelayanan & Kesehatan' => ['obat', 'sakit', 'rawat', 'medis', 'gizi', 'sehat', 'kesehatan', 'perawat', 'dokter', 'rumah sakit', 'klinik'],
|
||||||
} elseif (preg_match('/(mesin|bengkel|listrik|las|robot|motor|teknik|otomasi|elektronik)/', $minatRaw)) {
|
'Manajemen & Bisnis' => ['bisnis', 'uang', 'jual', 'kantor', 'hitung', 'ekonomi', 'dagang', 'usaha', 'entrepreneur', 'manager', 'marketing', 'akuntan'],
|
||||||
return 'Mesin & Listrik';
|
'Mesin & Listrik' => ['mesin', 'bengkel', 'listrik', 'las', 'robot', 'motor', 'teknik', 'otomasi', 'elektronik', 'maintenance'],
|
||||||
|
];
|
||||||
|
|
||||||
|
// Score setiap kategori berdasarkan keyword coverage
|
||||||
|
$scores = [];
|
||||||
|
foreach ($categoryKeywords as $category => $keywords) {
|
||||||
|
$scores[$category] = $this->keywordCoverage($minatNormalized, $keywords);
|
||||||
}
|
}
|
||||||
return 'Umum';
|
|
||||||
|
// Return kategori dengan coverage tertinggi
|
||||||
|
$bestCategory = 'Umum';
|
||||||
|
$maxScore = 0;
|
||||||
|
foreach ($scores as $category => $score) {
|
||||||
|
if ($score > $maxScore) {
|
||||||
|
$maxScore = $score;
|
||||||
|
$bestCategory = $category;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jika tidak ada keyword match, return Umum
|
||||||
|
return $maxScore > 0 ? $bestCategory : 'Umum';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pemetaan cita-cita ke kategori jurusan
|
* Pemetaan cita-cita ke kategori jurusan yang relevan
|
||||||
|
* Mengevaluasi input cita-cita dengan lebih detail
|
||||||
*/
|
*/
|
||||||
private function mapCitaCita(string $citaRaw): string
|
private function mapCitaCita(string $citaRaw): string
|
||||||
{
|
{
|
||||||
// Return raw mapped text untuk matching dengan keywords
|
// Normalize text untuk better matching
|
||||||
return $citaRaw;
|
$citaNormalized = $this->normalizeText($citaRaw);
|
||||||
|
|
||||||
|
// Map cita-cita ke category berdasarkan keywords
|
||||||
|
$careerCategories = [
|
||||||
|
'IT & Software' => ['programmer', 'developer', 'software', 'coding', 'hacker', 'web', 'database', 'it', 'engineer'],
|
||||||
|
'Agriculture' => ['petani', 'pertanian', 'agribisnis', 'kebun', 'ternak', 'peternak', 'agronomi'],
|
||||||
|
'Healthcare' => ['dokter', 'perawat', 'medis', 'gizi', 'terapis', 'farmasi', 'kesehatan'],
|
||||||
|
'Business' => ['entrepreneur', 'manager', 'marketing', 'sales', 'akuntan', 'keuangan', 'bisnis'],
|
||||||
|
'Engineering' => ['teknik', 'engineer', 'mesin', 'listrik', 'bengkel', 'maintenance', 'industri'],
|
||||||
|
'Communication' => ['jurnalis', 'komunikator', 'presenter', 'content', 'pariwisata', 'hospitality'],
|
||||||
|
];
|
||||||
|
|
||||||
|
// Score setiap kategori
|
||||||
|
$scores = [];
|
||||||
|
foreach ($careerCategories as $category => $keywords) {
|
||||||
|
$scores[$category] = $this->keywordCoverage($citaNormalized, $keywords);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return kategori dengan coverage tertinggi
|
||||||
|
$bestCategory = 'Umum';
|
||||||
|
$maxScore = 0;
|
||||||
|
foreach ($scores as $category => $score) {
|
||||||
|
if ($score > $maxScore) {
|
||||||
|
$maxScore = $score;
|
||||||
|
$bestCategory = $category;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $maxScore > 0 ? $bestCategory : 'Umum';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -434,6 +609,7 @@ private function analyzePrestasi(string $prestasiRaw): array
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Skor atribut teks berdasarkan coverage keyword (0..1) lalu dipetakan menjadi likelihood.
|
* Skor atribut teks berdasarkan coverage keyword (0..1) lalu dipetakan menjadi likelihood.
|
||||||
|
* Digunakan untuk cita-cita dan prestasi scoring.
|
||||||
*/
|
*/
|
||||||
private function scoreKeywordLikelihood(string $text, array $keywords, float $matchProb): float
|
private function scoreKeywordLikelihood(string $text, array $keywords, float $matchProb): float
|
||||||
{
|
{
|
||||||
|
|
@ -443,6 +619,16 @@ private function scoreKeywordLikelihood(string $text, array $keywords, float $ma
|
||||||
|
|
||||||
$coverage = $this->keywordCoverage($text, $keywords);
|
$coverage = $this->keywordCoverage($text, $keywords);
|
||||||
|
|
||||||
|
// Log untuk debugging
|
||||||
|
if ($coverage > 0) {
|
||||||
|
\Log::debug('Keyword Coverage', [
|
||||||
|
'text' => $text,
|
||||||
|
'keywords_count' => count($keywords),
|
||||||
|
'coverage' => $coverage,
|
||||||
|
'match_prob' => $matchProb,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
// Base 0.2 agar tidak 0 total, lalu naik proporsional coverage.
|
// Base 0.2 agar tidak 0 total, lalu naik proporsional coverage.
|
||||||
$likelihood = 0.20 + ($coverage * ($matchProb - 0.20));
|
$likelihood = 0.20 + ($coverage * ($matchProb - 0.20));
|
||||||
|
|
||||||
|
|
@ -451,20 +637,23 @@ private function scoreKeywordLikelihood(string $text, array $keywords, float $ma
|
||||||
|
|
||||||
private function scoreMinatLikelihood(string $minatRaw, string $minatMapped, string $targetMinat, float $matchProb): float
|
private function scoreMinatLikelihood(string $minatRaw, string $minatMapped, string $targetMinat, float $matchProb): float
|
||||||
{
|
{
|
||||||
|
// Expanded keyword bank dengan lebih banyak variasi
|
||||||
$keywordBank = [
|
$keywordBank = [
|
||||||
'Logika & Komputer' => ['coding', 'programming', 'komputer', 'software', 'web', 'data', 'ai', 'digital'],
|
'Logika & Komputer' => ['coding', 'programming', 'komputer', 'software', 'web', 'data', 'ai', 'digital', 'aplikasi', 'developer', 'coding', 'programer', 'it', 'database', 'network'],
|
||||||
'Alam & Tanaman' => ['pertanian', 'tanaman', 'kebun', 'sawah', 'alam', 'peternakan', 'agribisnis'],
|
'Alam & Tanaman' => ['pertanian', 'tanaman', 'kebun', 'sawah', 'alam', 'peternakan', 'agribisnis', 'hewan', 'ternak', 'panen', 'tani', 'petani', 'hortikultura'],
|
||||||
'Pelayanan & Kesehatan' => ['kesehatan', 'medis', 'gizi', 'perawat', 'dokter', 'klinik', 'rumah sakit'],
|
'Pelayanan & Kesehatan' => ['kesehatan', 'medis', 'gizi', 'perawat', 'dokter', 'klinik', 'rumah sakit', 'farmasi', 'terapis', 'kesehatan masyarakat', 'kesehatan', 'sehat', 'rawat'],
|
||||||
'Manajemen & Bisnis' => ['bisnis', 'usaha', 'marketing', 'keuangan', 'manajemen', 'akuntansi', 'entrepreneur'],
|
'Manajemen & Bisnis' => ['bisnis', 'usaha', 'marketing', 'keuangan', 'manajemen', 'akuntansi', 'entrepreneur', 'sales', 'marketing', 'penjualan', 'perbankan', 'akuntan'],
|
||||||
'Mesin & Listrik' => ['mesin', 'listrik', 'teknik', 'otomasi', 'elektronik', 'mekanik', 'industri'],
|
'Mesin & Listrik' => ['mesin', 'listrik', 'teknik', 'otomasi', 'elektronik', 'mekanik', 'industri', 'bengkel', 'las', 'motor', 'teknis'],
|
||||||
];
|
];
|
||||||
|
|
||||||
$targetKeywords = $keywordBank[$targetMinat] ?? [];
|
$targetKeywords = $keywordBank[$targetMinat] ?? [];
|
||||||
$coverage = $this->keywordCoverage($minatRaw, $targetKeywords);
|
$coverage = $this->keywordCoverage($minatRaw, $targetKeywords);
|
||||||
|
|
||||||
|
// Perfect match jika mapped minat sama dengan target
|
||||||
$categoryMatch = ($minatMapped === $targetMinat) ? 1.0 : 0.0;
|
$categoryMatch = ($minatMapped === $targetMinat) ? 1.0 : 0.0;
|
||||||
|
|
||||||
// Kombinasi semantic match + category match.
|
// Weighted combination: kategori match lebih penting (60%) daripada coverage (40%)
|
||||||
$combined = (0.6 * $coverage) + (0.4 * $categoryMatch);
|
$combined = (0.6 * $categoryMatch) + (0.4 * $coverage);
|
||||||
$likelihood = 0.20 + ($combined * ($matchProb - 0.20));
|
$likelihood = 0.20 + ($combined * ($matchProb - 0.20));
|
||||||
|
|
||||||
return max(0.05, min(0.98, $likelihood));
|
return max(0.05, min(0.98, $likelihood));
|
||||||
|
|
|
||||||
|
|
@ -73,10 +73,26 @@
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.input-error {
|
||||||
|
border-color: #ef4444 !important;
|
||||||
|
background-color: #fef2f2 !important;
|
||||||
|
}
|
||||||
|
.error-message {
|
||||||
|
color: #dc2626;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-top: 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
animation: slideIn 0.3s ease-out;
|
||||||
|
}
|
||||||
|
.error-message::before {
|
||||||
|
content: "⚠️";
|
||||||
|
}
|
||||||
@keyframes slideIn {
|
@keyframes slideIn {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(10px);
|
transform: translateY(-5px);
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|
@ -165,28 +181,63 @@
|
||||||
|
|
||||||
<!-- Input Area -->
|
<!-- Input Area -->
|
||||||
<div class="border-t border-gray-200 pt-3 sm:pt-4">
|
<div class="border-t border-gray-200 pt-3 sm:pt-4">
|
||||||
<form id="chatForm" class="flex gap-2">
|
<form id="chatForm" class="space-y-2">
|
||||||
@csrf
|
@csrf
|
||||||
<input
|
<div class="flex gap-2">
|
||||||
type="text"
|
<div class="flex-1 relative">
|
||||||
id="messageInput"
|
<input
|
||||||
name="message"
|
type="text"
|
||||||
placeholder="Ketik pertanyaan..."
|
id="messageInput"
|
||||||
class="flex-1 px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm"
|
name="message"
|
||||||
required
|
placeholder="Ketik pertanyaan..."
|
||||||
>
|
class="w-full px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-maroon text-sm transition-colors"
|
||||||
<button
|
autocomplete="off"
|
||||||
type="submit"
|
>
|
||||||
class="gradient-maroon text-white font-bold py-2 px-4 sm:px-6 rounded-lg hover:opacity-90 transition text-sm sm:text-base whitespace-nowrap"
|
<div id="errorMessage" class="error-message hidden"></div>
|
||||||
id="sendBtn"
|
</div>
|
||||||
>
|
<button
|
||||||
Kirim
|
type="submit"
|
||||||
</button>
|
class="gradient-maroon text-white font-bold py-2 px-4 sm:px-6 rounded-lg hover:opacity-90 transition text-sm sm:text-base whitespace-nowrap disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
id="sendBtn"
|
||||||
|
>
|
||||||
|
Kirim
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- History Chat Section -->
|
||||||
|
@php
|
||||||
|
$chatHistories = $chatHistories ?? [];
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
@if(count($chatHistories) > 0)
|
||||||
|
<div class="mt-8 sm:mt-12">
|
||||||
|
<div class="bg-white rounded-lg shadow-lg p-5 sm:p-8 border-l-4 border-orange-500">
|
||||||
|
<div class="flex items-center gap-3 mb-6">
|
||||||
|
<div class="w-12 h-12 sm:w-14 sm:h-14 rounded-lg bg-orange-100 flex items-center justify-center text-2xl flex-shrink-0">💬</div>
|
||||||
|
<div>
|
||||||
|
<h2 class="text-lg sm:text-xl md:text-2xl font-bold text-maroon">Riwayat Chat</h2>
|
||||||
|
<p class="text-xs sm:text-sm text-gray-600">Percakapan Anda dengan AI sebelumnya</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-3 sm:space-y-4 max-h-96 overflow-y-auto">
|
||||||
|
@foreach($chatHistories as $chat)
|
||||||
|
<a href="{{ route('chatbot.index', ['session' => $chat->id_sesi]) }}" class="block border border-gray-200 rounded-lg p-4 hover:shadow-md hover:border-maroon transition">
|
||||||
|
<p class="text-xs sm:text-sm text-gray-500 mb-2">{{ $chat->created_at->format('d M Y - H:i') }}</p>
|
||||||
|
<p class="text-xs sm:text-sm text-gray-700 line-clamp-2 leading-relaxed">
|
||||||
|
<span class="font-semibold">Anda:</span> {{ $chat->prompt ?? 'Tidak ada pesan' }}
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
@ -226,10 +277,47 @@ class="gradient-maroon text-white font-bold py-2 px-4 sm:px-6 rounded-lg hover:o
|
||||||
const messageInput = document.getElementById('messageInput');
|
const messageInput = document.getElementById('messageInput');
|
||||||
const chatContainer = document.getElementById('chatContainer');
|
const chatContainer = document.getElementById('chatContainer');
|
||||||
const sendBtn = document.getElementById('sendBtn');
|
const sendBtn = document.getElementById('sendBtn');
|
||||||
|
const errorMessage = document.getElementById('errorMessage');
|
||||||
|
|
||||||
// Track conversation history for multi-turn context
|
// Track conversation history for multi-turn context
|
||||||
let conversationHistory = [];
|
let conversationHistory = [];
|
||||||
|
|
||||||
|
// Validasi input saat user mengetik
|
||||||
|
messageInput.addEventListener('input', function() {
|
||||||
|
const message = this.value.trim();
|
||||||
|
|
||||||
|
if (message === '') {
|
||||||
|
// Show error state
|
||||||
|
this.classList.add('input-error');
|
||||||
|
errorMessage.textContent = 'Pesan tidak boleh kosong';
|
||||||
|
errorMessage.classList.remove('hidden');
|
||||||
|
sendBtn.disabled = true;
|
||||||
|
} else {
|
||||||
|
// Clear error state
|
||||||
|
this.classList.remove('input-error');
|
||||||
|
errorMessage.classList.add('hidden');
|
||||||
|
sendBtn.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validasi saat blur
|
||||||
|
messageInput.addEventListener('blur', function() {
|
||||||
|
if (this.value.trim() === '') {
|
||||||
|
this.classList.add('input-error');
|
||||||
|
errorMessage.textContent = 'Pesan tidak boleh kosong';
|
||||||
|
errorMessage.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear error saat focus
|
||||||
|
messageInput.addEventListener('focus', function() {
|
||||||
|
if (this.value.trim() === '') {
|
||||||
|
this.classList.add('input-error');
|
||||||
|
errorMessage.textContent = 'Ketik sesuatu untuk melanjutkan';
|
||||||
|
errorMessage.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Load previous messages if resuming session
|
// Load previous messages if resuming session
|
||||||
if (previousMessages.length > 0) {
|
if (previousMessages.length > 0) {
|
||||||
previousMessages.forEach(function(msg) {
|
previousMessages.forEach(function(msg) {
|
||||||
|
|
@ -238,11 +326,26 @@ class="gradient-maroon text-white font-bold py-2 px-4 sm:px-6 rounded-lg hover:o
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize button state
|
||||||
|
sendBtn.disabled = true;
|
||||||
|
|
||||||
chatForm.addEventListener('submit', async (e) => {
|
chatForm.addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const message = messageInput.value.trim();
|
const message = messageInput.value.trim();
|
||||||
if (!message) return;
|
|
||||||
|
// Validasi pesan tidak kosong
|
||||||
|
if (!message) {
|
||||||
|
messageInput.classList.add('input-error');
|
||||||
|
errorMessage.textContent = 'Pesan tidak boleh kosong, ketik sesuatu';
|
||||||
|
errorMessage.classList.remove('hidden');
|
||||||
|
messageInput.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear error sebelum mengirim
|
||||||
|
messageInput.classList.remove('input-error');
|
||||||
|
errorMessage.classList.add('hidden');
|
||||||
|
|
||||||
// Add user message to UI
|
// Add user message to UI
|
||||||
addMessage(message, 'user');
|
addMessage(message, 'user');
|
||||||
|
|
@ -306,8 +409,10 @@ class="gradient-maroon text-white font-bold py-2 px-4 sm:px-6 rounded-lg hover:o
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
addMessage('Terjadi kesalahan koneksi. Silakan coba lagi.', 'ai');
|
addMessage('Terjadi kesalahan koneksi. Silakan coba lagi.', 'ai');
|
||||||
} finally {
|
} finally {
|
||||||
sendBtn.disabled = false;
|
|
||||||
sendBtn.textContent = 'Kirim';
|
sendBtn.textContent = 'Kirim';
|
||||||
|
// Re-enable button only if input has text
|
||||||
|
const currentMessage = messageInput.value.trim();
|
||||||
|
sendBtn.disabled = currentMessage === '';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -154,55 +154,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- History Grid -->
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 sm:gap-8 mb-8 sm:mb-16">
|
|
||||||
<!-- History Rekomendasi Card -->
|
|
||||||
<div class="card-hover bg-white rounded-lg shadow-lg p-5 sm:p-8 border-l-4 border-purple-500 flex flex-col h-full">
|
|
||||||
<div class="flex items-start gap-3 sm:gap-4 mb-3 sm:mb-4 flex-grow">
|
|
||||||
<div class="text-3xl sm:text-4xl flex-shrink-0">📋</div>
|
|
||||||
<div>
|
|
||||||
<h3 class="text-lg sm:text-lg md:text-2xl font-bold text-maroon mb-1 sm:mb-2">History Rekomendasi</h3>
|
|
||||||
<p class="text-xs sm:text-sm md:text-base text-gray-700">
|
|
||||||
Lihat semua hasil analisis rekomendasi yang telah Anda lakukan sebelumnya.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-4 sm:mt-6">
|
|
||||||
<a href="{{ url('/history/rekomendasi') }}" class="block w-full text-center bg-purple-600 hover:bg-purple-700 text-white font-bold py-2 sm:py-3 px-4 sm:px-6 rounded-lg transition duration-200 text-sm sm:text-base">
|
|
||||||
Lihat History
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="mt-3 sm:mt-4 p-2 sm:p-3 bg-purple-50 rounded-lg">
|
|
||||||
<p class="text-xs sm:text-sm text-purple-800">
|
|
||||||
<strong>Total:</strong> {{ $recommendationCount ?? 0 }} analisis
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- History Chat Card -->
|
|
||||||
<div class="card-hover bg-white rounded-lg shadow-lg p-5 sm:p-8 flex flex-col h-full" style="border-left: 4px solid #EA580C;">
|
|
||||||
<div class="flex items-start gap-3 sm:gap-4 mb-3 sm:mb-4 flex-grow">
|
|
||||||
<div class="text-3xl sm:text-4xl flex-shrink-0">💾</div>
|
|
||||||
<div>
|
|
||||||
<h3 class="text-lg sm:text-lg md:text-2xl font-bold text-maroon mb-1 sm:mb-2">History Chat</h3>
|
|
||||||
<p class="text-xs sm:text-sm md:text-base text-gray-700">
|
|
||||||
Lihat riwayat semua percakapan Anda dengan AI chatbot.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-4 sm:mt-6">
|
|
||||||
<a href="{{ url('/history/chat') }}" class="block w-full text-center text-white font-bold py-2 sm:py-3 px-4 sm:px-6 rounded-lg transition duration-200 text-sm sm:text-base" style="background-color: #EA580C;">
|
|
||||||
Lihat History
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="mt-3 sm:mt-4 p-2 sm:p-3 rounded-lg" style="background-color: #FFF7ED;">
|
|
||||||
<p class="text-xs sm:text-sm" style="color: #9A3412;">
|
|
||||||
<strong>Total:</strong> {{ $chatCount ?? 0 }} chat
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Info Section -->
|
<!-- Info Section -->
|
||||||
<div class="bg-white rounded-lg shadow-lg p-5 sm:p-8 mb-6 sm:mb-8">
|
<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 md:text-2xl font-bold text-maroon mb-4 sm:mb-6">9 Jurusan Tersedia</h3>
|
<h3 class="text-lg sm:text-xl md:text-2xl font-bold text-maroon mb-4 sm:mb-6">9 Jurusan Tersedia</h3>
|
||||||
|
|
|
||||||
|
|
@ -243,44 +243,6 @@ class="input-focus w-full border border-gray-300 rounded-lg px-4 py-2 text-sm fo
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- ========== HAPUS AKUN ========== --}}
|
|
||||||
<div class="bg-white rounded-lg shadow-lg p-6 sm:p-8 border-l-4 border-red-500">
|
|
||||||
<h2 class="text-xl font-bold text-red-700 mb-1">⚠️ Hapus Akun</h2>
|
|
||||||
<p class="text-sm text-gray-500 mb-4">
|
|
||||||
Setelah akun dihapus, semua data dan riwayat Anda akan dihapus secara permanen. Pastikan Anda sudah menyimpan data yang diperlukan.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<button type="button" onclick="document.getElementById('delete-section').classList.toggle('hidden')" class="bg-red-600 hover:bg-red-700 text-white font-bold py-2 px-6 rounded-lg text-sm transition">
|
|
||||||
🗑️ Hapus Akun Saya
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{{-- Konfirmasi Hapus --}}
|
|
||||||
<div id="delete-section" class="hidden mt-6 p-4 bg-red-50 border border-red-200 rounded-lg">
|
|
||||||
<p class="text-sm text-red-800 mb-4 font-semibold">⚠️ Apakah Anda yakin? Masukkan password untuk konfirmasi:</p>
|
|
||||||
<form method="POST" action="{{ route('profile.destroy') }}">
|
|
||||||
@csrf
|
|
||||||
@method('delete')
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<input type="password" name="password" placeholder="Masukkan password Anda"
|
|
||||||
class="input-focus w-full border border-red-300 rounded-lg px-4 py-2 text-sm focus:ring-0">
|
|
||||||
@error('password', 'userDeletion')
|
|
||||||
<p class="text-red-600 text-xs mt-1">{{ $message }}</p>
|
|
||||||
@enderror
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex gap-3">
|
|
||||||
<button type="button" onclick="document.getElementById('delete-section').classList.add('hidden')" class="bg-gray-300 hover:bg-gray-400 text-gray-800 font-bold py-2 px-4 rounded-lg text-sm transition">
|
|
||||||
Batal
|
|
||||||
</button>
|
|
||||||
<button type="submit" class="bg-red-700 hover:bg-red-800 text-white font-bold py-2 px-4 rounded-lg text-sm transition">
|
|
||||||
Ya, Hapus Akun
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,42 @@
|
||||||
border-color: #5B7B89;
|
border-color: #5B7B89;
|
||||||
box-shadow: 0 0 0 3px rgba(91, 123, 137, 0.1);
|
box-shadow: 0 0 0 3px rgba(91, 123, 137, 0.1);
|
||||||
}
|
}
|
||||||
|
.input-error {
|
||||||
|
border-color: #ef4444 !important;
|
||||||
|
background-color: #fef2f2 !important;
|
||||||
|
}
|
||||||
|
.input-valid {
|
||||||
|
border-color: #10b981 !important;
|
||||||
|
background-color: #f0fdf4 !important;
|
||||||
|
}
|
||||||
|
.error-icon::before {
|
||||||
|
content: "⚠️ ";
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
}
|
||||||
|
.success-icon::before {
|
||||||
|
content: "✅ ";
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
}
|
||||||
|
.input-wrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.validation-message {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
animation: slideIn 0.3s ease-out;
|
||||||
|
}
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-cream">
|
<body class="bg-cream">
|
||||||
|
|
@ -67,13 +103,19 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if ($errors->any())
|
@if ($errors->any())
|
||||||
<div class="bg-red-50 border-l-4 border-red-500 p-4 sm:p-5 rounded-lg mb-6 shadow-sm">
|
<div class="bg-red-50 border-l-4 border-red-500 p-4 sm:p-5 rounded-lg mb-6 shadow-md animate-pulse">
|
||||||
<h3 class="text-red-700 font-bold text-sm sm:text-base mb-3">❌ Kesalahan Validasi:</h3>
|
<div class="flex items-start gap-3">
|
||||||
<ul class="list-disc list-inside space-y-1 text-red-600 text-xs sm:text-sm">
|
<span class="text-2xl flex-shrink-0">❌</span>
|
||||||
@foreach ($errors->all() as $error)
|
<div class="flex-1">
|
||||||
<li>{{ $error }}</li>
|
<h3 class="text-red-700 font-bold text-sm sm:text-base mb-3">Terjadi Kesalahan Validasi</h3>
|
||||||
@endforeach
|
<p class="text-red-600 text-xs sm:text-sm mb-3">Silakan perbaiki kesalahan berikut sebelum melanjutkan:</p>
|
||||||
</ul>
|
<ul class="list-disc list-inside space-y-2 text-red-600 text-xs sm:text-sm bg-white bg-opacity-50 p-3 rounded">
|
||||||
|
@foreach ($errors->all() as $error)
|
||||||
|
<li class="ml-2">{{ $error }}</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
|
|
@ -96,72 +138,72 @@
|
||||||
@if(isset($student) && $student->kelompok_asal == 'IPA')
|
@if(isset($student) && $student->kelompok_asal == 'IPA')
|
||||||
{{-- SISWA IPA: Matematika, Fisika, Kimia, Biologi --}}
|
{{-- SISWA IPA: Matematika, Fisika, Kimia, Biologi --}}
|
||||||
<div class="grid grid-cols-2 lg:grid-cols-4 gap-2 sm:gap-3">
|
<div class="grid grid-cols-2 lg:grid-cols-4 gap-2 sm:gap-3">
|
||||||
<div>
|
<div class="input-wrapper">
|
||||||
<label for="mtk" class="block text-xs sm:text-sm font-semibold text-gray-700 mb-1">Matematika <span class="text-red-500">*</span></label>
|
<label for="mtk" class="block text-xs sm:text-sm font-semibold text-gray-700 mb-1">Matematika <span class="text-red-500">*</span></label>
|
||||||
<input id="mtk" type="number" name="mtk" min="0" max="100" value="{{ old('mtk') }}" placeholder="85" required
|
<input id="mtk" type="number" name="mtk" min="0" max="100" value="{{ old('mtk') }}" placeholder="Nilai: 85" required
|
||||||
class="block w-full px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm @error('mtk') border-red-500 @enderror">
|
class="block w-full px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm transition-colors @error('mtk') input-error @enderror">
|
||||||
@error('mtk')
|
@error('mtk')
|
||||||
<span class="text-red-500 text-xs mt-1">{{ $message }}</span>
|
<span class="validation-message text-red-600">⚠️ {{ $message }}</span>
|
||||||
@enderror
|
@enderror
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="input-wrapper">
|
||||||
<label for="fisika" class="block text-xs sm:text-sm font-semibold text-gray-700 mb-1">Fisika <span class="text-red-500">*</span></label>
|
<label for="fisika" class="block text-xs sm:text-sm font-semibold text-gray-700 mb-1">Fisika <span class="text-red-500">*</span></label>
|
||||||
<input id="fisika" type="number" name="fisika" min="0" max="100" value="{{ old('fisika') }}" placeholder="78" required
|
<input id="fisika" type="number" name="fisika" min="0" max="100" value="{{ old('fisika') }}" placeholder="Nilai: 78" required
|
||||||
class="block w-full px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm @error('fisika') border-red-500 @enderror">
|
class="block w-full px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm transition-colors @error('fisika') input-error @enderror">
|
||||||
@error('fisika')
|
@error('fisika')
|
||||||
<span class="text-red-500 text-xs mt-1">{{ $message }}</span>
|
<span class="validation-message text-red-600">⚠️ {{ $message }}</span>
|
||||||
@enderror
|
@enderror
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="input-wrapper">
|
||||||
<label for="kimia" class="block text-xs sm:text-sm font-semibold text-gray-700 mb-1">Kimia <span class="text-red-500">*</span></label>
|
<label for="kimia" class="block text-xs sm:text-sm font-semibold text-gray-700 mb-1">Kimia <span class="text-red-500">*</span></label>
|
||||||
<input id="kimia" type="number" name="kimia" min="0" max="100" value="{{ old('kimia') }}" placeholder="72" required
|
<input id="kimia" type="number" name="kimia" min="0" max="100" value="{{ old('kimia') }}" placeholder="Nilai: 72" required
|
||||||
class="block w-full px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm @error('kimia') border-red-500 @enderror">
|
class="block w-full px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm transition-colors @error('kimia') input-error @enderror">
|
||||||
@error('kimia')
|
@error('kimia')
|
||||||
<span class="text-red-500 text-xs mt-1">{{ $message }}</span>
|
<span class="validation-message text-red-600">⚠️ {{ $message }}</span>
|
||||||
@enderror
|
@enderror
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="input-wrapper">
|
||||||
<label for="biologi" class="block text-xs sm:text-sm font-semibold text-gray-700 mb-1">Biologi <span class="text-red-500">*</span></label>
|
<label for="biologi" class="block text-xs sm:text-sm font-semibold text-gray-700 mb-1">Biologi <span class="text-red-500">*</span></label>
|
||||||
<input id="biologi" type="number" name="biologi" min="0" max="100" value="{{ old('biologi') }}" placeholder="80" required
|
<input id="biologi" type="number" name="biologi" min="0" max="100" value="{{ old('biologi') }}" placeholder="Nilai: 80" required
|
||||||
class="block w-full px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm @error('biologi') border-red-500 @enderror">
|
class="block w-full px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm transition-colors @error('biologi') input-error @enderror">
|
||||||
@error('biologi')
|
@error('biologi')
|
||||||
<span class="text-red-500 text-xs mt-1">{{ $message }}</span>
|
<span class="validation-message text-red-600">⚠️ {{ $message }}</span>
|
||||||
@enderror
|
@enderror
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
{{-- SISWA IPS: Ekonomi, Geografi, Sosiologi, Sejarah --}}
|
{{-- SISWA IPS: Ekonomi, Geografi, Sosiologi, Sejarah --}}
|
||||||
<div class="grid grid-cols-2 lg:grid-cols-4 gap-2 sm:gap-3">
|
<div class="grid grid-cols-2 lg:grid-cols-4 gap-2 sm:gap-3">
|
||||||
<div>
|
<div class="input-wrapper">
|
||||||
<label for="ekonomi" class="block text-xs sm:text-sm font-semibold text-gray-700 mb-1">Ekonomi <span class="text-red-500">*</span></label>
|
<label for="ekonomi" class="block text-xs sm:text-sm font-semibold text-gray-700 mb-1">Ekonomi <span class="text-red-500">*</span></label>
|
||||||
<input id="ekonomi" type="number" name="ekonomi" min="0" max="100" value="{{ old('ekonomi') }}" placeholder="82" required
|
<input id="ekonomi" type="number" name="ekonomi" min="0" max="100" value="{{ old('ekonomi') }}" placeholder="Nilai: 82" required
|
||||||
class="block w-full px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm @error('ekonomi') border-red-500 @enderror">
|
class="block w-full px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm transition-colors @error('ekonomi') input-error @enderror">
|
||||||
@error('ekonomi')
|
@error('ekonomi')
|
||||||
<span class="text-red-500 text-xs mt-1">{{ $message }}</span>
|
<span class="validation-message text-red-600">⚠️ {{ $message }}</span>
|
||||||
@enderror
|
@enderror
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="input-wrapper">
|
||||||
<label for="geografi" class="block text-xs sm:text-sm font-semibold text-gray-700 mb-1">Geografi <span class="text-red-500">*</span></label>
|
<label for="geografi" class="block text-xs sm:text-sm font-semibold text-gray-700 mb-1">Geografi <span class="text-red-500">*</span></label>
|
||||||
<input id="geografi" type="number" name="geografi" min="0" max="100" value="{{ old('geografi') }}" placeholder="76" required
|
<input id="geografi" type="number" name="geografi" min="0" max="100" value="{{ old('geografi') }}" placeholder="Nilai: 76" required
|
||||||
class="block w-full px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm @error('geografi') border-red-500 @enderror">
|
class="block w-full px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm transition-colors @error('geografi') input-error @enderror">
|
||||||
@error('geografi')
|
@error('geografi')
|
||||||
<span class="text-red-500 text-xs mt-1">{{ $message }}</span>
|
<span class="validation-message text-red-600">⚠️ {{ $message }}</span>
|
||||||
@enderror
|
@enderror
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="input-wrapper">
|
||||||
<label for="sosiologi" class="block text-xs sm:text-sm font-semibold text-gray-700 mb-1">Sosiologi <span class="text-red-500">*</span></label>
|
<label for="sosiologi" class="block text-xs sm:text-sm font-semibold text-gray-700 mb-1">Sosiologi <span class="text-red-500">*</span></label>
|
||||||
<input id="sosiologi" type="number" name="sosiologi" min="0" max="100" value="{{ old('sosiologi') }}" placeholder="74" required
|
<input id="sosiologi" type="number" name="sosiologi" min="0" max="100" value="{{ old('sosiologi') }}" placeholder="Nilai: 74" required
|
||||||
class="block w-full px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm @error('sosiologi') border-red-500 @enderror">
|
class="block w-full px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm transition-colors @error('sosiologi') input-error @enderror">
|
||||||
@error('sosiologi')
|
@error('sosiologi')
|
||||||
<span class="text-red-500 text-xs mt-1">{{ $message }}</span>
|
<span class="validation-message text-red-600">⚠️ {{ $message }}</span>
|
||||||
@enderror
|
@enderror
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="input-wrapper">
|
||||||
<label for="sejarah" class="block text-xs sm:text-sm font-semibold text-gray-700 mb-1">Sejarah <span class="text-red-500">*</span></label>
|
<label for="sejarah" class="block text-xs sm:text-sm font-semibold text-gray-700 mb-1">Sejarah <span class="text-red-500">*</span></label>
|
||||||
<input id="sejarah" type="number" name="sejarah" min="0" max="100" value="{{ old('sejarah') }}" placeholder="70" required
|
<input id="sejarah" type="number" name="sejarah" min="0" max="100" value="{{ old('sejarah') }}" placeholder="Nilai: 70" required
|
||||||
class="block w-full px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm @error('sejarah') border-red-500 @enderror">
|
class="block w-full px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm transition-colors @error('sejarah') input-error @enderror">
|
||||||
@error('sejarah')
|
@error('sejarah')
|
||||||
<span class="text-red-500 text-xs mt-1">{{ $message }}</span>
|
<span class="validation-message text-red-600">⚠️ {{ $message }}</span>
|
||||||
@enderror
|
@enderror
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -175,13 +217,13 @@ class="block w-full px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus-ma
|
||||||
<div class="p-4 sm:p-5 rounded-lg border-2 border-gray-200 bg-gray-50">
|
<div class="p-4 sm:p-5 rounded-lg border-2 border-gray-200 bg-gray-50">
|
||||||
<h3 class="font-bold text-maroon text-base sm:text-lg mb-1 sm:mb-2">2. Minat Siswa <span class="text-red-500">*</span></h3>
|
<h3 class="font-bold text-maroon text-base sm:text-lg mb-1 sm:mb-2">2. Minat Siswa <span class="text-red-500">*</span></h3>
|
||||||
<p class="text-xs text-gray-600 mb-3 sm:mb-4">Tuliskan bidang atau kegiatan yang Anda minati / sukai.</p>
|
<p class="text-xs text-gray-600 mb-3 sm:mb-4">Tuliskan bidang atau kegiatan yang Anda minati / sukai.</p>
|
||||||
<div>
|
<div class="input-wrapper">
|
||||||
<label for="minat" class="block text-xs sm:text-sm font-semibold text-gray-700 mb-1">Bidang Minat</label>
|
<label for="minat" class="block text-xs sm:text-sm font-semibold text-gray-700 mb-1">Bidang Minat</label>
|
||||||
<input id="minat" type="text" name="minat" value="{{ old('minat') }}" placeholder="Contoh: coding, komputer, bisnis, pertanian"
|
<input id="minat" type="text" name="minat" value="{{ old('minat') }}" placeholder="Contoh: coding, komputer, bisnis, pertanian"
|
||||||
class="block w-full px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm @error('minat') border-red-500 @enderror" required>
|
class="block w-full px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm transition-colors @error('minat') input-error @enderror" required>
|
||||||
<p class="text-xs text-gray-500 mt-1">Pisahkan dengan koma jika lebih dari satu minat</p>
|
<p class="text-xs text-gray-500 mt-1">Pisahkan dengan koma jika lebih dari satu minat</p>
|
||||||
@error('minat')
|
@error('minat')
|
||||||
<span class="text-red-500 text-xs mt-1">{{ $message }}</span>
|
<span class="validation-message text-red-600">⚠️ {{ $message }}</span>
|
||||||
@enderror
|
@enderror
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -194,9 +236,9 @@ class="block w-full px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus-ma
|
||||||
<p class="text-xs text-gray-600 mb-3 sm:mb-4">
|
<p class="text-xs text-gray-600 mb-3 sm:mb-4">
|
||||||
Bagian ini menanyakan arah lanjutan studi Anda setelah lulus SMA, yaitu ingin melanjutkan ke rumpun jurusan Politeknik Negeri Jember yang mana. Jadi fokusnya adalah tujuan jalur jurusan yang ingin dituju, bukan metode belajar.
|
Bagian ini menanyakan arah lanjutan studi Anda setelah lulus SMA, yaitu ingin melanjutkan ke rumpun jurusan Politeknik Negeri Jember yang mana. Jadi fokusnya adalah tujuan jalur jurusan yang ingin dituju, bukan metode belajar.
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div class="input-wrapper">
|
||||||
<label for="pref_studi" class="block text-xs sm:text-sm font-semibold text-gray-700 mb-1">Arah Rumpun Jurusan Tujuan</label>
|
<label for="pref_studi" class="block text-xs sm:text-sm font-semibold text-gray-700 mb-1">Arah Rumpun Jurusan Tujuan</label>
|
||||||
<select id="pref_studi" name="pref_studi" class="block w-full px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm @error('pref_studi') border-red-500 @enderror" required>
|
<select id="pref_studi" name="pref_studi" class="block w-full px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm transition-colors @error('pref_studi') input-error @enderror" required>
|
||||||
<option value="">-- Pilih Arah Rumpun Jurusan --</option>
|
<option value="">-- Pilih Arah Rumpun Jurusan --</option>
|
||||||
<option value="Sains & Teknologi" {{ old('pref_studi') == 'Sains & Teknologi' ? 'selected' : '' }}>Sains & Teknologi (contoh: TI, Teknik)</option>
|
<option value="Sains & Teknologi" {{ old('pref_studi') == 'Sains & Teknologi' ? 'selected' : '' }}>Sains & Teknologi (contoh: TI, Teknik)</option>
|
||||||
<option value="Pertanian & Lingkungan" {{ old('pref_studi') == 'Pertanian & Lingkungan' ? 'selected' : '' }}>Pertanian & Lingkungan (contoh: Produksi/Teknologi Pertanian)</option>
|
<option value="Pertanian & Lingkungan" {{ old('pref_studi') == 'Pertanian & Lingkungan' ? 'selected' : '' }}>Pertanian & Lingkungan (contoh: Produksi/Teknologi Pertanian)</option>
|
||||||
|
|
@ -206,7 +248,7 @@ class="block w-full px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus-ma
|
||||||
</select>
|
</select>
|
||||||
<p class="text-xs text-gray-500 mt-1">Pilih rumpun yang paling menggambarkan jurusan Polije yang ingin Anda tuju setelah lulus.</p>
|
<p class="text-xs text-gray-500 mt-1">Pilih rumpun yang paling menggambarkan jurusan Polije yang ingin Anda tuju setelah lulus.</p>
|
||||||
@error('pref_studi')
|
@error('pref_studi')
|
||||||
<span class="text-red-500 text-xs mt-1">{{ $message }}</span>
|
<span class="validation-message text-red-600">⚠️ {{ $message }}</span>
|
||||||
@enderror
|
@enderror
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -217,13 +259,13 @@ class="block w-full px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus-ma
|
||||||
<div class="p-4 sm:p-5 rounded-lg border-2 border-gray-200 bg-gray-50">
|
<div class="p-4 sm:p-5 rounded-lg border-2 border-gray-200 bg-gray-50">
|
||||||
<h3 class="font-bold text-maroon text-base sm:text-lg mb-1 sm:mb-2">4. Cita-cita / Preferensi Karir <span class="text-red-500">*</span></h3>
|
<h3 class="font-bold text-maroon text-base sm:text-lg mb-1 sm:mb-2">4. Cita-cita / Preferensi Karir <span class="text-red-500">*</span></h3>
|
||||||
<p class="text-xs text-gray-600 mb-3 sm:mb-4">Tuliskan profesi atau karir yang Anda impikan.</p>
|
<p class="text-xs text-gray-600 mb-3 sm:mb-4">Tuliskan profesi atau karir yang Anda impikan.</p>
|
||||||
<div>
|
<div class="input-wrapper">
|
||||||
<label for="cita_cita" class="block text-xs sm:text-sm font-semibold text-gray-700 mb-1">Cita-cita</label>
|
<label for="cita_cita" class="block text-xs sm:text-sm font-semibold text-gray-700 mb-1">Cita-cita</label>
|
||||||
<input id="cita_cita" type="text" name="cita_cita" value="{{ old('cita_cita') }}" placeholder="Contoh: programmer, dokter, pengusaha"
|
<input id="cita_cita" type="text" name="cita_cita" value="{{ old('cita_cita') }}" placeholder="Contoh: programmer, dokter, pengusaha"
|
||||||
class="block w-full px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm @error('cita_cita') border-red-500 @enderror" required>
|
class="block w-full px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm transition-colors @error('cita_cita') input-error @enderror" required>
|
||||||
<p class="text-xs text-gray-500 mt-1">Bisa lebih dari satu, pisahkan dengan koma</p>
|
<p class="text-xs text-gray-500 mt-1">Bisa lebih dari satu, pisahkan dengan koma</p>
|
||||||
@error('cita_cita')
|
@error('cita_cita')
|
||||||
<span class="text-red-500 text-xs mt-1">{{ $message }}</span>
|
<span class="validation-message text-red-600">⚠️ {{ $message }}</span>
|
||||||
@enderror
|
@enderror
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -234,30 +276,75 @@ class="block w-full px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus-ma
|
||||||
<div class="p-4 sm:p-5 rounded-lg border-2 border-gray-200 bg-gray-50">
|
<div class="p-4 sm:p-5 rounded-lg border-2 border-gray-200 bg-gray-50">
|
||||||
<h3 class="font-bold text-maroon text-base sm:text-lg mb-1 sm:mb-2">5. Prestasi Akademik / Non-Akademik</h3>
|
<h3 class="font-bold text-maroon text-base sm:text-lg mb-1 sm:mb-2">5. Prestasi Akademik / Non-Akademik</h3>
|
||||||
<p class="text-xs text-gray-600 mb-3 sm:mb-4">Tuliskan prestasi yang pernah diraih (opsional).</p>
|
<p class="text-xs text-gray-600 mb-3 sm:mb-4">Tuliskan prestasi yang pernah diraih (opsional).</p>
|
||||||
<div>
|
<div class="input-wrapper">
|
||||||
<label for="prestasi" class="block text-xs sm:text-sm font-semibold text-gray-700 mb-1">Prestasi</label>
|
<label for="prestasi" class="block text-xs sm:text-sm font-semibold text-gray-700 mb-1">Prestasi</label>
|
||||||
<input id="prestasi" type="text" name="prestasi" value="{{ old('prestasi') }}" placeholder="Contoh: Juara 1 olimpiade MTK, sertifikat web design"
|
<input id="prestasi" type="text" name="prestasi" value="{{ old('prestasi') }}" placeholder="Contoh: Juara 1 olimpiade MTK, sertifikat web design"
|
||||||
class="block w-full px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm @error('prestasi') border-red-500 @enderror">
|
class="block w-full px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm transition-colors @error('prestasi') input-error @enderror">
|
||||||
<p class="text-xs text-gray-500 mt-1">Kosongkan jika belum ada prestasi</p>
|
<p class="text-xs text-gray-500 mt-1">Kosongkan jika belum ada prestasi</p>
|
||||||
@error('prestasi')
|
@error('prestasi')
|
||||||
<span class="text-red-500 text-xs mt-1">{{ $message }}</span>
|
<span class="validation-message text-red-600">⚠️ {{ $message }}</span>
|
||||||
@enderror
|
@enderror
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Submit Button -->
|
<!-- Submit Button -->
|
||||||
<div class="mt-4 sm:mt-6 p-3 sm:p-4 rounded-lg bg-gray-50 border border-gray-200 text-center">
|
<div class="mt-4 sm:mt-6 p-3 sm:p-4 rounded-lg bg-gradient-to-r from-yellow-50 to-yellow-100 border border-yellow-200 text-center">
|
||||||
<p class="text-xs sm:text-sm text-gray-600 mb-3 sm:mb-4">
|
<p class="text-xs sm:text-sm text-gray-600 mb-3 sm:mb-4">
|
||||||
Setelah menekan tombol, sistem akan menganalisis data Anda dan menampilkan ranking 9 jurusan.
|
⏳ Setelah menekan tombol, sistem akan menganalisis data Anda dan menampilkan ranking 9 jurusan.
|
||||||
</p>
|
</p>
|
||||||
<button type="submit" class="w-full gradient-maroon text-white font-bold py-2 sm:py-3 px-4 sm:px-6 rounded-lg hover:opacity-90 transition duration-200 text-sm sm:text-base">
|
<button type="submit" class="w-full gradient-maroon text-white font-bold py-2 sm:py-3 px-4 sm:px-6 rounded-lg hover:opacity-90 active:scale-95 transition duration-200 text-sm sm:text-base shadow-lg disabled:opacity-50 disabled:cursor-not-allowed">
|
||||||
Lihat Rekomendasi Jurusan
|
✨ Lihat Rekomendasi Jurusan
|
||||||
</button>
|
</button>
|
||||||
|
<p class="text-xs sm:text-sm text-gray-500 mt-3">Pastikan semua data terisi dengan benar sebelum melanjutkan</p>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Riwayat Rekomendasi Section -->
|
||||||
|
@php
|
||||||
|
$recommendations = $recommendations ?? [];
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
@if(count($recommendations) > 0)
|
||||||
|
<div class="mt-8 sm:mt-12">
|
||||||
|
<div class="bg-white rounded-lg shadow-lg p-5 sm:p-8 border-l-4 border-purple-500">
|
||||||
|
<div class="flex items-center gap-3 mb-6">
|
||||||
|
<div class="w-12 h-12 sm:w-14 sm:h-14 rounded-lg bg-purple-100 flex items-center justify-center text-2xl flex-shrink-0">📋</div>
|
||||||
|
<div>
|
||||||
|
<h2 class="text-lg sm:text-xl md:text-2xl font-bold text-maroon">Riwayat Rekomendasi</h2>
|
||||||
|
<p class="text-xs sm:text-sm text-gray-600">Analisis yang sudah Anda lakukan sebelumnya</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-3 sm:space-y-4">
|
||||||
|
@foreach($recommendations as $rec)
|
||||||
|
<div class="border border-gray-200 rounded-lg p-4 hover:shadow-md hover:border-maroon transition cursor-pointer" onclick="this.classList.toggle('expanded')">
|
||||||
|
<div class="flex justify-between items-start mb-2">
|
||||||
|
<div class="flex-1">
|
||||||
|
<p class="text-xs sm:text-sm text-gray-500">{{ $rec->created_at->format('d M Y - H:i') }}</p>
|
||||||
|
<p class="text-sm sm:text-base font-bold text-maroon mt-1">Rekomendasi Utama: <span class="text-lg">{{ $rec->hasil_rekomendasi[0]['jurusan'] ?? 'N/A' }}</span></p>
|
||||||
|
</div>
|
||||||
|
<span class="inline-block px-3 py-1 rounded-full text-xs font-bold bg-purple-100 text-purple-700 flex-shrink-0 ml-2">
|
||||||
|
{{ number_format(($rec->hasil_rekomendasi[0]['skor'] ?? 0) * 100, 1) }}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-gray-600 mb-3">Top 3 Rekomendasi:</p>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-2">
|
||||||
|
@foreach(array_slice($rec->hasil_rekomendasi, 0, 3) as $idx => $rec_item)
|
||||||
|
<div class="bg-gradient-to-br from-purple-50 to-purple-100 p-3 rounded text-center">
|
||||||
|
<p class="text-xs text-gray-600">{{ $idx + 1 }}. {{ $rec_item['jurusan'] }}</p>
|
||||||
|
<p class="text-sm font-bold text-maroon">{{ number_format($rec_item['skor'] * 100, 1) }}%</p>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
<!-- Info Metode -->
|
<!-- Info Metode -->
|
||||||
<div class="mt-6 sm:mt-8 p-3 sm:p-4 bg-white rounded-lg border border-gray-200 shadow-sm">
|
<div class="mt-6 sm:mt-8 p-3 sm:p-4 bg-white rounded-lg border border-gray-200 shadow-sm">
|
||||||
<p class="text-xs sm:text-sm text-gray-600">
|
<p class="text-xs sm:text-sm text-gray-600">
|
||||||
|
|
@ -265,5 +352,126 @@ class="block w-full px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus-ma
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
|
||||||
</html>
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const form = document.querySelector('form');
|
||||||
|
const submitBtn = form?.querySelector('button[type="submit"]');
|
||||||
|
|
||||||
|
// Validasi input fields
|
||||||
|
const inputs = form?.querySelectorAll('input, select, textarea');
|
||||||
|
|
||||||
|
inputs?.forEach(input => {
|
||||||
|
// Validasi pada change event
|
||||||
|
input.addEventListener('change', function() {
|
||||||
|
validateField(this);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validasi pada blur event
|
||||||
|
input.addEventListener('blur', function() {
|
||||||
|
validateField(this);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove error on input
|
||||||
|
input.addEventListener('input', function() {
|
||||||
|
if (this.classList.contains('input-error')) {
|
||||||
|
validateField(this);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validasi saat submit
|
||||||
|
form?.addEventListener('submit', function(e) {
|
||||||
|
let isValid = true;
|
||||||
|
|
||||||
|
inputs?.forEach(input => {
|
||||||
|
if (!validateField(input)) {
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
e.preventDefault();
|
||||||
|
// Scroll ke error pertama
|
||||||
|
const firstError = form.querySelector('.input-error');
|
||||||
|
if (firstError) {
|
||||||
|
firstError.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
firstError.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function validateField(field) {
|
||||||
|
const value = field.value.trim();
|
||||||
|
const isRequired = field.hasAttribute('required');
|
||||||
|
const name = field.name;
|
||||||
|
const type = field.type;
|
||||||
|
let errorMsg = '';
|
||||||
|
let isValid = true;
|
||||||
|
|
||||||
|
// Remove previous error/valid styling
|
||||||
|
field.classList.remove('input-error', 'input-valid');
|
||||||
|
const existingMsg = field.parentElement.querySelector('.validation-message');
|
||||||
|
if (existingMsg) {
|
||||||
|
existingMsg.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validasi untuk field yang required
|
||||||
|
if (isRequired && !value) {
|
||||||
|
errorMsg = '⚠️ ' + field.placeholder?.split('Contoh')[0]?.trim() + ' tidak boleh kosong';
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validasi untuk number fields
|
||||||
|
if (type === 'number' && value) {
|
||||||
|
const numValue = parseFloat(value);
|
||||||
|
const min = parseFloat(field.min || 0);
|
||||||
|
const max = parseFloat(field.max || 100);
|
||||||
|
|
||||||
|
if (isNaN(numValue)) {
|
||||||
|
errorMsg = '⚠️ Masukkan angka yang valid (0-100)';
|
||||||
|
isValid = false;
|
||||||
|
} else if (numValue < min || numValue > max) {
|
||||||
|
errorMsg = '⚠️ Nilai harus antara ' + min + ' - ' + max;
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validasi untuk text fields (min length)
|
||||||
|
if ((name === 'minat' || name === 'cita_cita') && value && value.length < 3) {
|
||||||
|
errorMsg = '⚠️ Minimal 3 karakter, jelaskan lebih detail';
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validasi untuk select (pref_studi)
|
||||||
|
if (name === 'pref_studi' && !value) {
|
||||||
|
errorMsg = '⚠️ Pilih salah satu preferensi studi';
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply styling
|
||||||
|
if (!isValid && value) {
|
||||||
|
field.classList.add('input-error');
|
||||||
|
} else if (isValid && (isRequired || value)) {
|
||||||
|
field.classList.add('input-valid');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show error message
|
||||||
|
if (errorMsg) {
|
||||||
|
const msgDiv = document.createElement('div');
|
||||||
|
msgDiv.className = 'validation-message text-red-600';
|
||||||
|
msgDiv.textContent = errorMsg;
|
||||||
|
field.parentElement.appendChild(msgDiv);
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValid || !isRequired;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loading state pada submit
|
||||||
|
form?.addEventListener('submit', function() {
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
submitBtn.innerHTML = '<span class="inline-flex items-center">⏳ Menganalisis data...</span>';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
use App\Http\Controllers\ChatbotController;
|
use App\Http\Controllers\ChatbotController;
|
||||||
use App\Http\Controllers\AdminController;
|
use App\Http\Controllers\AdminController;
|
||||||
use App\Http\Controllers\BKController;
|
use App\Http\Controllers\BKController;
|
||||||
|
use App\Http\Controllers\AlumniController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
|
@ -13,14 +14,7 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::get('/dashboard', function () {
|
Route::get('/dashboard', function () {
|
||||||
$user = Auth::user();
|
return view('dashboard');
|
||||||
$recommendationCount = $user ? \App\Models\Recommendation::where('user_id', $user->id)->count() : 0;
|
|
||||||
$chatCount = $user ? \App\Models\ChatHistory::where('user_id', $user->id)->count() : 0;
|
|
||||||
|
|
||||||
return view('dashboard', [
|
|
||||||
'recommendationCount' => $recommendationCount,
|
|
||||||
'chatCount' => $chatCount
|
|
||||||
]);
|
|
||||||
})->middleware(['auth', 'verified', 'roleRedirect'])->name('dashboard');
|
})->middleware(['auth', 'verified', 'roleRedirect'])->name('dashboard');
|
||||||
|
|
||||||
Route::middleware('auth')->group(function () {
|
Route::middleware('auth')->group(function () {
|
||||||
|
|
@ -60,6 +54,9 @@
|
||||||
Route::put('/jurusan/{id}', [AdminController::class, 'jurusanUpdate'])->name('jurusan.update');
|
Route::put('/jurusan/{id}', [AdminController::class, 'jurusanUpdate'])->name('jurusan.update');
|
||||||
Route::delete('/jurusan/{id}', [AdminController::class, 'jurusanDestroy'])->name('jurusan.destroy');
|
Route::delete('/jurusan/{id}', [AdminController::class, 'jurusanDestroy'])->name('jurusan.destroy');
|
||||||
|
|
||||||
|
// 3.5 Manajemen Data Alumni
|
||||||
|
Route::resource('alumni', AlumniController::class);
|
||||||
|
|
||||||
// 4. Manajemen Akun Guru BK
|
// 4. Manajemen Akun Guru BK
|
||||||
Route::get('/guru-bk', [AdminController::class, 'guruBK'])->name('guru-bk');
|
Route::get('/guru-bk', [AdminController::class, 'guruBK'])->name('guru-bk');
|
||||||
Route::get('/guru-bk/create', [AdminController::class, 'guruBKCreate'])->name('guru-bk.create');
|
Route::get('/guru-bk/create', [AdminController::class, 'guruBKCreate'])->name('guru-bk.create');
|
||||||
|
|
@ -94,6 +91,16 @@
|
||||||
Route::get('/jurusan/{id}/edit', [BKController::class, 'jurusanEdit'])->name('jurusan.edit');
|
Route::get('/jurusan/{id}/edit', [BKController::class, 'jurusanEdit'])->name('jurusan.edit');
|
||||||
Route::put('/jurusan/{id}', [BKController::class, 'jurusanUpdate'])->name('jurusan.update');
|
Route::put('/jurusan/{id}', [BKController::class, 'jurusanUpdate'])->name('jurusan.update');
|
||||||
Route::delete('/jurusan/{id}', [BKController::class, 'jurusanDestroy'])->name('jurusan.destroy');
|
Route::delete('/jurusan/{id}', [BKController::class, 'jurusanDestroy'])->name('jurusan.destroy');
|
||||||
|
|
||||||
|
// Alumni Routes untuk BK - sama seperti admin
|
||||||
|
Route::get('/alumni', [BKController::class, 'alumni'])->name('alumni');
|
||||||
|
Route::get('/alumni/create', [BKController::class, 'alumniCreate'])->name('alumni.create');
|
||||||
|
Route::post('/alumni', [BKController::class, 'alumniStore'])->name('alumni.store');
|
||||||
|
Route::get('/alumni/{alumni}', [BKController::class, 'alumniShow'])->name('alumni.show');
|
||||||
|
Route::get('/alumni/{alumni}/edit', [BKController::class, 'alumniEdit'])->name('alumni.edit');
|
||||||
|
Route::put('/alumni/{alumni}', [BKController::class, 'alumniUpdate'])->name('alumni.update');
|
||||||
|
Route::delete('/alumni/{alumni}', [BKController::class, 'alumniDestroy'])->name('alumni.destroy');
|
||||||
|
|
||||||
Route::get('/profil', [BKController::class, 'profil'])->name('profil');
|
Route::get('/profil', [BKController::class, 'profil'])->name('profil');
|
||||||
Route::put('/profil', [BKController::class, 'updateProfil'])->name('profil.update');
|
Route::put('/profil', [BKController::class, 'updateProfil'])->name('profil.update');
|
||||||
Route::put('/profil/password', [BKController::class, 'updatePassword'])->name('profil.password');
|
Route::put('/profil/password', [BKController::class, 'updatePassword'])->name('profil.password');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue