feat: add BK admin panel with complete CRUD operations and student management
- Create BKController with dashboard, students, chat history, recommendations, and jurusan management - Add 8 BK panel views: students list/detail, chat history, riwayat rekomendasi/chatbot, jurusan CRUD, profil - Add 15 BK routes with auth and isBK middleware protection - Update AdminSeeder with 8+ char passwords and valid email format - Add database migrations for chat history improvements - Update various controllers and models for BK panel integration - Remove sidebar Profil duplication, keep navbar dropdown only
This commit is contained in:
parent
d602dd3353
commit
e0b0c10ddc
|
|
@ -0,0 +1,319 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\PolijeMajor;
|
||||||
|
use App\Models\Recommendation;
|
||||||
|
use App\Models\ChatHistory;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class BKController extends Controller
|
||||||
|
{
|
||||||
|
// ============================================
|
||||||
|
// 1. DASHBOARD
|
||||||
|
// ============================================
|
||||||
|
public function dashboard()
|
||||||
|
{
|
||||||
|
$totalSiswa = User::where('role', 'siswa')->count();
|
||||||
|
$totalRekomendasi = Recommendation::count();
|
||||||
|
$totalChatHistory = ChatHistory::count();
|
||||||
|
$totalJurusan = PolijeMajor::count();
|
||||||
|
|
||||||
|
$recentStudents = User::where('role', 'siswa')
|
||||||
|
->orderBy('created_at', 'desc')
|
||||||
|
->take(5)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$recentRecommendations = Recommendation::with('user')
|
||||||
|
->orderBy('created_at', 'desc')
|
||||||
|
->take(5)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$kelompokStats = User::where('role', 'siswa')
|
||||||
|
->selectRaw('kelompok_asal, COUNT(*) as count')
|
||||||
|
->groupBy('kelompok_asal')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$topMajors = Recommendation::selectRaw("
|
||||||
|
JSON_EXTRACT(hasil_rekomendasi, '$[0].jurusan') as major_name,
|
||||||
|
COUNT(*) as count
|
||||||
|
")
|
||||||
|
->groupByRaw("JSON_EXTRACT(hasil_rekomendasi, '$[0].jurusan')")
|
||||||
|
->orderBy('count', 'desc')
|
||||||
|
->take(5)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return view('bk.dashboard', compact(
|
||||||
|
'totalSiswa',
|
||||||
|
'totalRekomendasi',
|
||||||
|
'totalChatHistory',
|
||||||
|
'totalJurusan',
|
||||||
|
'recentStudents',
|
||||||
|
'recentRecommendations',
|
||||||
|
'kelompokStats',
|
||||||
|
'topMajors'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// 2. DATA SISWA (Read + Update)
|
||||||
|
// ============================================
|
||||||
|
public function students(Request $request)
|
||||||
|
{
|
||||||
|
$query = User::where('role', 'siswa')
|
||||||
|
->withCount('recommendations', 'chatHistories');
|
||||||
|
|
||||||
|
if ($request->filled('search')) {
|
||||||
|
$search = $request->search;
|
||||||
|
$query->where(function ($q) use ($search) {
|
||||||
|
$q->where('name', 'like', "%{$search}%")
|
||||||
|
->orWhere('nis', 'like', "%{$search}%");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('kelompok')) {
|
||||||
|
$query->where('kelompok_asal', $request->kelompok);
|
||||||
|
}
|
||||||
|
|
||||||
|
$students = $query->orderBy('created_at', 'desc')->paginate(20);
|
||||||
|
|
||||||
|
return view('bk.students.index', compact('students'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function studentDetail($id)
|
||||||
|
{
|
||||||
|
$student = User::findOrFail($id);
|
||||||
|
$recommendations = Recommendation::where('user_id', $id)
|
||||||
|
->orderBy('created_at', 'desc')
|
||||||
|
->get();
|
||||||
|
$chatHistories = ChatHistory::where('user_id', $id)
|
||||||
|
->orderBy('created_at', 'desc')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return view('bk.students.detail', compact('student', 'recommendations', 'chatHistories'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function chatHistory($id)
|
||||||
|
{
|
||||||
|
$user = User::findOrFail($id);
|
||||||
|
$chatHistories = ChatHistory::where('user_id', $id)
|
||||||
|
->orderBy('created_at', 'asc')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return view('bk.chat-history', compact('user', 'chatHistories'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// 3. HASIL REKOMENDASI JURUSAN
|
||||||
|
// ============================================
|
||||||
|
public function riwayatRekomendasi(Request $request)
|
||||||
|
{
|
||||||
|
$query = Recommendation::with('user');
|
||||||
|
|
||||||
|
if ($request->filled('search')) {
|
||||||
|
$search = $request->search;
|
||||||
|
$query->whereHas('user', function ($q) use ($search) {
|
||||||
|
$q->where('name', 'like', "%{$search}%");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$recommendations = $query->orderBy('created_at', 'desc')->paginate(20);
|
||||||
|
|
||||||
|
$uniqueStudents = Recommendation::distinct('user_id')->count('user_id');
|
||||||
|
|
||||||
|
$topMajorRow = Recommendation::selectRaw("
|
||||||
|
JSON_EXTRACT(hasil_rekomendasi, '$[0].jurusan') as major_name,
|
||||||
|
COUNT(*) as count
|
||||||
|
")
|
||||||
|
->groupByRaw("JSON_EXTRACT(hasil_rekomendasi, '$[0].jurusan')")
|
||||||
|
->orderBy('count', 'desc')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$topMajor = $topMajorRow ? trim($topMajorRow->major_name, '"') : null;
|
||||||
|
|
||||||
|
return view('bk.riwayat-rekomendasi.index', compact('recommendations', 'uniqueStudents', 'topMajor'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// 4. RIWAYAT KONSULTASI CHATBOT
|
||||||
|
// ============================================
|
||||||
|
public function riwayatChatbot(Request $request)
|
||||||
|
{
|
||||||
|
$query = ChatHistory::with('user');
|
||||||
|
|
||||||
|
if ($request->filled('search')) {
|
||||||
|
$search = $request->search;
|
||||||
|
$query->where(function ($q) use ($search) {
|
||||||
|
$q->where('prompt', 'like', "%{$search}%")
|
||||||
|
->orWhere('response', 'like', "%{$search}%")
|
||||||
|
->orWhereHas('user', function ($q2) use ($search) {
|
||||||
|
$q2->where('name', 'like', "%{$search}%");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$chatHistories = $query->orderBy('created_at', 'desc')->paginate(20);
|
||||||
|
|
||||||
|
$uniqueStudents = ChatHistory::distinct('user_id')->count('user_id');
|
||||||
|
$todayCount = ChatHistory::whereDate('created_at', today())->count();
|
||||||
|
|
||||||
|
return view('bk.riwayat-chatbot.index', compact('chatHistories', 'uniqueStudents', 'todayCount'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// 5. MANAJEMEN JURUSAN (CRUD)
|
||||||
|
// ============================================
|
||||||
|
public function jurusan()
|
||||||
|
{
|
||||||
|
$jurusanList = PolijeMajor::orderBy('nama_jurusan')->get();
|
||||||
|
return view('bk.jurusan.index', compact('jurusanList'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function jurusanCreate()
|
||||||
|
{
|
||||||
|
return view('bk.jurusan.create');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function jurusanStore(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'nama_jurusan' => 'required|string|max:255|unique:polije_majors,nama_jurusan',
|
||||||
|
'deskripsi' => 'nullable|string|max:1000',
|
||||||
|
'keywords' => 'nullable|string',
|
||||||
|
'preferensi_studi' => 'nullable|string',
|
||||||
|
'prospek_kerja' => 'nullable|string|max:1000',
|
||||||
|
'bobot_mtk' => 'nullable|numeric|min:0|max:1',
|
||||||
|
'bobot_fisika' => 'nullable|numeric|min:0|max:1',
|
||||||
|
'bobot_kimia' => 'nullable|numeric|min:0|max:1',
|
||||||
|
'bobot_biologi' => 'nullable|numeric|min:0|max:1',
|
||||||
|
'bobot_ekonomi' => 'nullable|numeric|min:0|max:1',
|
||||||
|
'bobot_geografi' => 'nullable|numeric|min:0|max:1',
|
||||||
|
'bobot_sosiologi' => 'nullable|numeric|min:0|max:1',
|
||||||
|
'bobot_sejarah' => 'nullable|numeric|min:0|max:1',
|
||||||
|
]);
|
||||||
|
|
||||||
|
PolijeMajor::create([
|
||||||
|
'nama_jurusan' => $request->nama_jurusan,
|
||||||
|
'deskripsi' => $request->deskripsi,
|
||||||
|
'keywords' => $this->parseTagInput($request->keywords),
|
||||||
|
'preferensi_studi' => $this->parseTagInput($request->preferensi_studi),
|
||||||
|
'prospek_kerja' => $request->prospek_kerja,
|
||||||
|
'bobot_mapel' => $this->parseBobotMapel($request),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->route('bk.jurusan')->with('success', 'Jurusan berhasil ditambahkan!');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function jurusanEdit($id)
|
||||||
|
{
|
||||||
|
$jurusan = PolijeMajor::findOrFail($id);
|
||||||
|
return view('bk.jurusan.edit', compact('jurusan'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function jurusanUpdate(Request $request, $id)
|
||||||
|
{
|
||||||
|
$jurusan = PolijeMajor::findOrFail($id);
|
||||||
|
|
||||||
|
$request->validate([
|
||||||
|
'nama_jurusan' => ['required', 'string', 'max:255', Rule::unique('polije_majors', 'nama_jurusan')->ignore($jurusan->id)],
|
||||||
|
'deskripsi' => 'nullable|string|max:1000',
|
||||||
|
'keywords' => 'nullable|string',
|
||||||
|
'preferensi_studi' => 'nullable|string',
|
||||||
|
'prospek_kerja' => 'nullable|string|max:1000',
|
||||||
|
'bobot_mtk' => 'nullable|numeric|min:0|max:1',
|
||||||
|
'bobot_fisika' => 'nullable|numeric|min:0|max:1',
|
||||||
|
'bobot_kimia' => 'nullable|numeric|min:0|max:1',
|
||||||
|
'bobot_biologi' => 'nullable|numeric|min:0|max:1',
|
||||||
|
'bobot_ekonomi' => 'nullable|numeric|min:0|max:1',
|
||||||
|
'bobot_geografi' => 'nullable|numeric|min:0|max:1',
|
||||||
|
'bobot_sosiologi' => 'nullable|numeric|min:0|max:1',
|
||||||
|
'bobot_sejarah' => 'nullable|numeric|min:0|max:1',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$jurusan->update([
|
||||||
|
'nama_jurusan' => $request->nama_jurusan,
|
||||||
|
'deskripsi' => $request->deskripsi,
|
||||||
|
'keywords' => $this->parseTagInput($request->keywords),
|
||||||
|
'preferensi_studi' => $this->parseTagInput($request->preferensi_studi),
|
||||||
|
'prospek_kerja' => $request->prospek_kerja,
|
||||||
|
'bobot_mapel' => $this->parseBobotMapel($request),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->route('bk.jurusan')->with('success', 'Jurusan berhasil diperbarui!');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function jurusanDestroy($id)
|
||||||
|
{
|
||||||
|
$jurusan = PolijeMajor::findOrFail($id);
|
||||||
|
$jurusan->delete();
|
||||||
|
|
||||||
|
return redirect()->route('bk.jurusan')->with('success', 'Jurusan berhasil dihapus!');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function parseTagInput(?string $input): array
|
||||||
|
{
|
||||||
|
if (empty($input)) return [];
|
||||||
|
return array_values(array_filter(array_map('trim', explode(',', $input))));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function parseBobotMapel(Request $request): array
|
||||||
|
{
|
||||||
|
$mapelList = ['mtk', 'fisika', 'kimia', 'biologi', 'ekonomi', 'geografi', 'sosiologi', 'sejarah'];
|
||||||
|
$bobot = [];
|
||||||
|
foreach ($mapelList as $mapel) {
|
||||||
|
$value = $request->input("bobot_{$mapel}");
|
||||||
|
if (!is_null($value) && $value !== '') {
|
||||||
|
$bobot[$mapel] = floatval($value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $bobot;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// 6. PROFIL GURU BK
|
||||||
|
// ============================================
|
||||||
|
public function profil()
|
||||||
|
{
|
||||||
|
$guru = Auth::user();
|
||||||
|
return view('bk.profil.index', compact('guru'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateProfil(Request $request)
|
||||||
|
{
|
||||||
|
$guru = Auth::user();
|
||||||
|
|
||||||
|
$request->validate([
|
||||||
|
'name' => 'required|string|max:255',
|
||||||
|
'email' => ['required', 'email', Rule::unique('users')->ignore($guru->id)],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$guru->name = $request->name;
|
||||||
|
$guru->email = $request->email;
|
||||||
|
$guru->save();
|
||||||
|
|
||||||
|
return redirect()->route('bk.profil')->with('success', 'Profil berhasil diperbarui!');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updatePassword(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'current_password' => 'required',
|
||||||
|
'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();
|
||||||
|
|
||||||
|
return redirect()->route('bk.profil')->with('success', 'Password berhasil diubah!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,8 +4,10 @@
|
||||||
|
|
||||||
use App\Services\GeminiService;
|
use App\Services\GeminiService;
|
||||||
use App\Models\ChatHistory;
|
use App\Models\ChatHistory;
|
||||||
|
use App\Models\Recommendation;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
class ChatbotController extends Controller
|
class ChatbotController extends Controller
|
||||||
{
|
{
|
||||||
|
|
@ -17,88 +19,132 @@ public function __construct(GeminiService $geminiService)
|
||||||
$this->geminiService = $geminiService;
|
$this->geminiService = $geminiService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function index()
|
/**
|
||||||
|
* Tampilkan halaman chatbot.
|
||||||
|
* Jika ada ?session=xxx, lanjutkan sesi lama (pakai rekomendasi saat itu).
|
||||||
|
* Jika ada ?rec=xxx, buat sesi baru terkait rekomendasi tertentu.
|
||||||
|
* Jika tidak ada parameter, buat sesi baru dengan rekomendasi terbaru.
|
||||||
|
*/
|
||||||
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
$recentRecommendation = session('recomendation_data', null);
|
$sessionId = $request->query('session');
|
||||||
|
$recId = $request->query('rec');
|
||||||
|
$previousMessages = [];
|
||||||
|
$recommendationId = null;
|
||||||
|
|
||||||
// Jika session kosong, ambil rekomendasi terakhir dari database
|
if ($sessionId) {
|
||||||
if (!$recentRecommendation) {
|
// Lanjutkan sesi lama — ambil semua chat dari sesi ini
|
||||||
$lastRec = \App\Models\Recommendation::where('user_id', $user->id)
|
$chats = ChatHistory::where('user_id', $user->id)
|
||||||
->latest()
|
->where('session_id', $sessionId)
|
||||||
->first();
|
->orderBy('created_at', 'asc')
|
||||||
|
->get();
|
||||||
|
|
||||||
if ($lastRec) {
|
if ($chats->isEmpty()) {
|
||||||
$hasil = json_decode($lastRec->hasil_rekomendasi, true);
|
// Session tidak valid, buat baru
|
||||||
$topJurusan = $hasil[0] ?? null;
|
$sessionId = Str::uuid()->toString();
|
||||||
$recentRecommendation = [
|
} else {
|
||||||
'jurusan' => $topJurusan['jurusan'] ?? null,
|
// Ambil recommendation_id dari sesi ini
|
||||||
'skor' => $topJurusan['skor'] ?? null,
|
$recommendationId = $chats->first()->recommendation_id;
|
||||||
'nilai' => $lastRec->nilai_akademik,
|
|
||||||
'minat' => $lastRec->minat,
|
foreach ($chats as $chat) {
|
||||||
'pref_studi' => $lastRec->preferensi_studi,
|
$previousMessages[] = [
|
||||||
];
|
'role' => 'user',
|
||||||
|
'text' => $chat->prompt,
|
||||||
|
];
|
||||||
|
$previousMessages[] = [
|
||||||
|
'role' => 'ai',
|
||||||
|
'text' => $this->stripMarkdown($chat->response),
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Sesi baru
|
||||||
|
$sessionId = Str::uuid()->toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tentukan recommendation_id:
|
||||||
|
// 1. Dari sesi lama (sudah diset di atas)
|
||||||
|
// 2. Dari parameter ?rec= (klik dari hasil rekomendasi)
|
||||||
|
// 3. Dari rekomendasi terbaru user
|
||||||
|
if (!$recommendationId && $recId) {
|
||||||
|
$rec = Recommendation::where('id', $recId)
|
||||||
|
->where('user_id', $user->id)
|
||||||
|
->first();
|
||||||
|
$recommendationId = $rec ? $rec->id : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$recommendationId) {
|
||||||
|
$latestRec = Recommendation::where('user_id', $user->id)->latest()->first();
|
||||||
|
$recommendationId = $latestRec ? $latestRec->id : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ambil konteks rekomendasi berdasarkan ID spesifik
|
||||||
|
$recentRecommendation = $this->getRecommendationContext($user, $recommendationId);
|
||||||
|
|
||||||
return view('chatbot.index', [
|
return view('chatbot.index', [
|
||||||
'recommendation' => $recentRecommendation
|
'recommendation' => $recentRecommendation,
|
||||||
|
'sessionId' => $sessionId,
|
||||||
|
'previousMessages' => $previousMessages,
|
||||||
|
'recommendationId' => $recommendationId,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kirim pesan dan terima respons AI
|
||||||
|
*/
|
||||||
public function send(Request $request)
|
public function send(Request $request)
|
||||||
{
|
{
|
||||||
$request->validate([
|
$request->validate([
|
||||||
'message' => 'required|string|max:1000',
|
'message' => 'required|string|max:1000',
|
||||||
|
'sessionId' => 'required|string|max:36',
|
||||||
|
'recommendationId' => 'nullable|integer',
|
||||||
'chatHistory' => 'nullable|array|max:20',
|
'chatHistory' => 'nullable|array|max:20',
|
||||||
'chatHistory.*.role' => 'required|string|in:user,ai',
|
'chatHistory.*.role' => 'required|string|in:user,ai',
|
||||||
'chatHistory.*.text' => 'required|string|max:2000',
|
'chatHistory.*.text' => 'required|string|max:2000',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$message = $request->input('message');
|
$message = $request->input('message');
|
||||||
|
$sessionId = $request->input('sessionId');
|
||||||
|
$recommendationId = $request->input('recommendationId');
|
||||||
$chatHistory = $request->input('chatHistory', []);
|
$chatHistory = $request->input('chatHistory', []);
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
$recentRecommendation = session('recomendation_data', []);
|
|
||||||
|
|
||||||
// Jika session kosong, ambil rekomendasi terakhir dari database
|
// Ambil konteks rekomendasi berdasarkan ID spesifik sesi ini
|
||||||
if (empty($recentRecommendation)) {
|
$recentRecommendation = $this->getRecommendationContext($user, $recommendationId) ?? [];
|
||||||
$lastRec = \App\Models\Recommendation::where('user_id', $user->id)
|
|
||||||
->latest()
|
|
||||||
->first();
|
|
||||||
|
|
||||||
if ($lastRec) {
|
|
||||||
$hasil = json_decode($lastRec->hasil_rekomendasi, true);
|
|
||||||
$topJurusan = $hasil[0] ?? null;
|
|
||||||
$recentRecommendation = [
|
|
||||||
'jurusan' => $topJurusan['jurusan'] ?? null,
|
|
||||||
'skor' => $topJurusan['skor'] ?? null,
|
|
||||||
'nilai' => $lastRec->nilai_akademik,
|
|
||||||
'minat' => $lastRec->minat,
|
|
||||||
'pref_studi' => $lastRec->preferensi_studi,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Siapkan context untuk Gemini
|
// Siapkan context untuk Gemini
|
||||||
$context = [
|
$context = [
|
||||||
'recommendation' => $recentRecommendation['jurusan'] ?? null,
|
'recommendation' => $recentRecommendation['jurusan'] ?? null,
|
||||||
'score' => isset($recentRecommendation['skor']) ? number_format($recentRecommendation['skor'] * 100, 1) : null,
|
'score' => isset($recentRecommendation['skor']) ? number_format(($recentRecommendation['skor'] > 1 ? $recentRecommendation['skor'] : $recentRecommendation['skor'] * 100), 1) : null,
|
||||||
|
'top3' => $recentRecommendation['top3'] ?? [],
|
||||||
'profile' => [
|
'profile' => [
|
||||||
'nama' => $user->name,
|
'nama' => $user->name,
|
||||||
'kelompok' => $user->kelompok_asal,
|
'kelompok' => $user->kelompok_asal ?? null,
|
||||||
'nilai' => $recentRecommendation['nilai'] ?? null,
|
'nilai' => $recentRecommendation['nilai'] ?? null,
|
||||||
|
'rata_rata' => $recentRecommendation['rata_rata'] ?? null,
|
||||||
'minat' => $recentRecommendation['minat'] ?? null,
|
'minat' => $recentRecommendation['minat'] ?? null,
|
||||||
'pref' => $recentRecommendation['pref_studi'] ?? null,
|
'pref' => $recentRecommendation['pref_studi'] ?? null,
|
||||||
|
'cita_cita' => $recentRecommendation['cita_cita'] ?? null,
|
||||||
|
'prestasi' => $recentRecommendation['prestasi'] ?? null,
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Cari pertanyaan serupa dari riwayat semua siswa
|
||||||
|
$similarQA = $this->findSimilarQuestions($message, $user->id);
|
||||||
|
if (!empty($similarQA)) {
|
||||||
|
$context['similar_qa'] = $similarQA;
|
||||||
|
}
|
||||||
|
|
||||||
// Panggil Gemini API dengan conversation history
|
// Panggil Gemini API dengan conversation history
|
||||||
$response = $this->geminiService->chat($message, $context, $chatHistory);
|
$response = $this->geminiService->chat($message, $context, $chatHistory);
|
||||||
|
|
||||||
// Simpan chat ke database
|
// Simpan chat ke database dengan session_id dan recommendation_id
|
||||||
if ($user && isset($response['message'])) {
|
if ($user && isset($response['message'])) {
|
||||||
ChatHistory::create([
|
ChatHistory::create([
|
||||||
'user_id' => $user->id,
|
'user_id' => $user->id,
|
||||||
|
'session_id' => $sessionId,
|
||||||
|
'recommendation_id' => $recommendationId,
|
||||||
'prompt' => $message,
|
'prompt' => $message,
|
||||||
'response' => $response['message'],
|
'response' => $response['message'],
|
||||||
]);
|
]);
|
||||||
|
|
@ -108,15 +154,189 @@ public function send(Request $request)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tampilkan history chat
|
* Tampilkan history chat, dikelompokkan per sesi
|
||||||
*/
|
*/
|
||||||
public function historyChat()
|
public function historyChat()
|
||||||
{
|
{
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|
||||||
|
// Ambil semua chat user dengan relasi recommendation
|
||||||
$chatHistories = ChatHistory::where('user_id', $user->id)
|
$chatHistories = ChatHistory::where('user_id', $user->id)
|
||||||
|
->with('recommendation')
|
||||||
->orderBy('created_at', 'desc')
|
->orderBy('created_at', 'desc')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
return view('history.chat', compact('chatHistories'));
|
// Kelompokkan per session_id
|
||||||
|
$sessions = $chatHistories->groupBy('session_id')->map(function ($chats, $sessionId) {
|
||||||
|
$first = $chats->last(); // oldest in group (karena desc)
|
||||||
|
$last = $chats->first(); // newest in group
|
||||||
|
$rec = $first->recommendation;
|
||||||
|
$recInfo = null;
|
||||||
|
if ($rec) {
|
||||||
|
$hasil = is_array($rec->hasil_rekomendasi)
|
||||||
|
? $rec->hasil_rekomendasi
|
||||||
|
: json_decode($rec->hasil_rekomendasi, true);
|
||||||
|
$topJurusan = $hasil[0] ?? null;
|
||||||
|
$recInfo = [
|
||||||
|
'id' => $rec->id,
|
||||||
|
'jurusan' => $topJurusan['jurusan'] ?? '-',
|
||||||
|
'skor' => $topJurusan['skor'] ?? 0,
|
||||||
|
'tanggal' => $rec->created_at,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
'session_id' => $sessionId,
|
||||||
|
'chats' => $chats->reverse()->values(),
|
||||||
|
'first_message' => Str::limit($first->prompt, 80),
|
||||||
|
'message_count' => $chats->count(),
|
||||||
|
'started_at' => $first->created_at,
|
||||||
|
'last_at' => $last->created_at,
|
||||||
|
'recommendation' => $recInfo,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
return view('history.chat', compact('sessions'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ambil konteks rekomendasi berdasarkan ID spesifik.
|
||||||
|
* Jika ID tidak ada, coba dari session, lalu dari DB (terbaru).
|
||||||
|
*/
|
||||||
|
private function getRecommendationContext($user, $recommendationId = null)
|
||||||
|
{
|
||||||
|
// Jika ada recommendation_id spesifik, ambil langsung dari DB
|
||||||
|
$lastRec = null;
|
||||||
|
|
||||||
|
if ($recommendationId) {
|
||||||
|
$lastRec = Recommendation::where('id', $recommendationId)
|
||||||
|
->where('user_id', $user->id)
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: dari session (saat baru selesai rekomendasi)
|
||||||
|
if (!$lastRec) {
|
||||||
|
$sessionData = session('recomendation_data', null);
|
||||||
|
if ($sessionData) {
|
||||||
|
return $sessionData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: rekomendasi terbaru dari DB
|
||||||
|
if (!$lastRec) {
|
||||||
|
$lastRec = Recommendation::where('user_id', $user->id)
|
||||||
|
->latest()
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$lastRec) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$hasil = is_array($lastRec->hasil_rekomendasi)
|
||||||
|
? $lastRec->hasil_rekomendasi
|
||||||
|
: json_decode($lastRec->hasil_rekomendasi, true);
|
||||||
|
$topJurusan = $hasil[0] ?? null;
|
||||||
|
$top3 = array_slice($hasil ?? [], 0, 3);
|
||||||
|
|
||||||
|
// Hitung rata-rata dari kolom nilai
|
||||||
|
$nilaiCols = ['mtk', 'fisika', 'kimia', 'biologi', 'ekonomi', 'geografi', 'sosiologi', 'sejarah'];
|
||||||
|
$validVals = array_filter(array_map(fn($c) => $lastRec->$c, $nilaiCols), fn($v) => $v !== null);
|
||||||
|
$rataRata = count($validVals) > 0 ? round(array_sum($validVals) / count($validVals), 1) : null;
|
||||||
|
|
||||||
|
// Kategorisasi
|
||||||
|
$katNilai = 'Rendah';
|
||||||
|
if ($rataRata >= 85) $katNilai = 'Tinggi';
|
||||||
|
elseif ($rataRata >= 70) $katNilai = 'Sedang';
|
||||||
|
|
||||||
|
return [
|
||||||
|
'jurusan' => $topJurusan['jurusan'] ?? null,
|
||||||
|
'skor' => $topJurusan['skor'] ?? null,
|
||||||
|
'detail' => $topJurusan['detail'] ?? [],
|
||||||
|
'nilai' => $katNilai,
|
||||||
|
'rata_rata' => $rataRata,
|
||||||
|
'minat' => $lastRec->minat,
|
||||||
|
'pref_studi' => $lastRec->preferensi_studi,
|
||||||
|
'cita_cita' => $lastRec->cita_cita,
|
||||||
|
'prestasi' => $lastRec->prestasi,
|
||||||
|
'top3' => array_map(fn($r) => [
|
||||||
|
'jurusan' => $r['jurusan'] ?? '',
|
||||||
|
'skor' => $r['skor'] ?? 0,
|
||||||
|
], $top3),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cari pertanyaan serupa dari riwayat chat semua user
|
||||||
|
* Menggunakan keyword matching sederhana
|
||||||
|
*/
|
||||||
|
private function findSimilarQuestions(string $message, int $currentUserId): array
|
||||||
|
{
|
||||||
|
$messageLower = strtolower($message);
|
||||||
|
|
||||||
|
// Ekstrak kata kunci penting (min 4 huruf, bukan stopwords)
|
||||||
|
$stopwords = ['yang', 'dari', 'untuk', 'dengan', 'dalam', 'pada', 'akan', 'bisa', 'juga', 'saya', 'kamu', 'anda', 'tidak', 'sudah', 'belum', 'mau', 'ingin', 'apa', 'bagaimana', 'kenapa', 'mengapa', 'apakah', 'tolong', 'dong', 'aku', 'ini', 'itu'];
|
||||||
|
|
||||||
|
$words = preg_split('/[\s,;.!?\-\/]+/', $messageLower);
|
||||||
|
$keywords = array_filter($words, function ($w) use ($stopwords) {
|
||||||
|
return strlen($w) >= 4 && !in_array($w, $stopwords);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (empty($keywords)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cari chat history yang mengandung kata kunci serupa
|
||||||
|
$query = ChatHistory::select('prompt', 'response', 'created_at')
|
||||||
|
->where('user_id', $currentUserId);
|
||||||
|
|
||||||
|
$query->where(function ($q) use ($keywords) {
|
||||||
|
foreach ($keywords as $keyword) {
|
||||||
|
$q->orWhere('prompt', 'like', "%{$keyword}%");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$candidates = $query->orderBy('created_at', 'desc')
|
||||||
|
->limit(20)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
if ($candidates->isEmpty()) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scoring: hitung berapa keyword yang cocok
|
||||||
|
$scored = [];
|
||||||
|
foreach ($candidates as $chat) {
|
||||||
|
$promptLower = strtolower($chat->prompt);
|
||||||
|
$matchCount = 0;
|
||||||
|
foreach ($keywords as $kw) {
|
||||||
|
if (stripos($promptLower, $kw) !== false) {
|
||||||
|
$matchCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$ratio = $matchCount / count($keywords);
|
||||||
|
if ($ratio >= 0.4) { // minimal 40% keyword cocok
|
||||||
|
$scored[] = [
|
||||||
|
'prompt' => $chat->prompt,
|
||||||
|
'response' => Str::limit($chat->response, 300),
|
||||||
|
'score' => $ratio,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by score desc, ambil top 3
|
||||||
|
usort($scored, fn($a, $b) => $b['score'] <=> $a['score']);
|
||||||
|
return array_slice($scored, 0, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strip markdown formatting
|
||||||
|
*/
|
||||||
|
private function stripMarkdown(string $text): string
|
||||||
|
{
|
||||||
|
$text = preg_replace('/\*\*(.*?)\*\*/s', '$1', $text);
|
||||||
|
$text = preg_replace('/\*(.*?)\*/s', '$1', $text);
|
||||||
|
$text = preg_replace('/^#{1,6}\s+/m', '', $text);
|
||||||
|
$text = preg_replace('/`(.*?)`/s', '$1', $text);
|
||||||
|
return $text;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,7 @@ class RekomendasiController extends Controller
|
||||||
{
|
{
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
// Ambil data siswa dari akun (kolom `nis`, `kelompok_asal` di tabel `users`)
|
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
// Jika masih ada model Student di beberapa kode lama, abaikan; gunakan properti di User
|
|
||||||
$student = null;
|
$student = null;
|
||||||
if ($user) {
|
if ($user) {
|
||||||
$student = (object) [
|
$student = (object) [
|
||||||
|
|
@ -28,49 +26,42 @@ public function index()
|
||||||
return view('rekomendasi.input', compact('student'));
|
return view('rekomendasi.input', compact('student'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ============================================================
|
||||||
|
* ALGORITMA NAIVE BAYES UNTUK REKOMENDASI JURUSAN
|
||||||
|
* Sesuai flowchart:
|
||||||
|
* 1. Input Data
|
||||||
|
* 2. Preprocessing Data
|
||||||
|
* 3. Tentukan Hipotesis (H)
|
||||||
|
* 4. Hitung Probabilitas Awal (Prior) P(H)
|
||||||
|
* 5. Hitung Likelihood P(X|H) per fitur
|
||||||
|
* 6. Hitung Probabilitas Gabungan (Rumus Naive Bayes)
|
||||||
|
* P(H|X) ∝ P(H) × P(X1|H) × P(X2|H) × ... × P(Xn|H)
|
||||||
|
* 7. Klasifikasi (Hasil Rekomendasi)
|
||||||
|
* ============================================================
|
||||||
|
*/
|
||||||
public function proses(Request $request)
|
public function proses(Request $request)
|
||||||
{
|
{
|
||||||
// --- VALIDATION ---
|
$epsilon = 1e-9;
|
||||||
// Tentukan kelompok asal siswa
|
|
||||||
$user = Auth::user();
|
|
||||||
$kelompok = $user->kelompok_asal ?? 'IPS';
|
|
||||||
|
|
||||||
// Validasi berbeda untuk IPA dan IPS
|
// ================================================================
|
||||||
$baseRules = [
|
// LANGKAH 1: INPUT DATA
|
||||||
'minat' => 'required|string|max:255',
|
// ================================================================
|
||||||
'cita_cita' => 'required|string|max:255',
|
$scores = $request->only(['mtk', 'fisika', 'kimia', 'biologi', 'ekonomi', 'geografi', 'sosiologi', 'sejarah']);
|
||||||
'pref_studi' => 'required|in:Sains & Teknologi,Pertanian & Lingkungan,Kesehatan & Ilmu Hayat,Bisnis & Manajemen,Sosial & Humaniora',
|
$minatRaw = strtolower(trim($request->minat ?? ''));
|
||||||
'prestasi' => 'nullable|string|max:255',
|
$prefStudi = $request->pref_studi ?? 'Sains & Teknologi';
|
||||||
];
|
$citaRaw = strtolower(trim($request->cita_cita ?? ''));
|
||||||
|
$prestasiRaw = strtolower(trim($request->prestasi ?? ''));
|
||||||
|
|
||||||
if ($kelompok === 'IPA') {
|
// ================================================================
|
||||||
$nilaiRules = [
|
// LANGKAH 2: PREPROCESSING DATA
|
||||||
'mtk' => 'required|numeric|between:0,100',
|
// ================================================================
|
||||||
'fisika' => 'required|numeric|between:0,100',
|
|
||||||
'kimia' => 'required|numeric|between:0,100',
|
|
||||||
'biologi' => 'required|numeric|between:0,100',
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
$nilaiRules = [
|
|
||||||
'ekonomi' => 'required|numeric|between:0,100',
|
|
||||||
'geografi' => 'required|numeric|between:0,100',
|
|
||||||
'sosiologi' => 'required|numeric|between:0,100',
|
|
||||||
'sejarah' => 'required|numeric|between:0,100',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$request->validate(array_merge($baseRules, $nilaiRules));
|
// 2a. Hitung rata-rata nilai
|
||||||
|
$validScores = array_filter($scores, fn($v) => $v !== null && $v !== '');
|
||||||
// --- 1. SKOR NILAI AKADEMIK (40%) - dikumpulkan dulu, dihitung per jurusan ---
|
|
||||||
if ($kelompok === 'IPA') {
|
|
||||||
$scores = $request->only(['mtk', 'fisika', 'kimia', 'biologi']);
|
|
||||||
} else {
|
|
||||||
$scores = $request->only(['ekonomi', 'geografi', 'sosiologi', 'sejarah']);
|
|
||||||
}
|
|
||||||
$validScores = array_filter($scores, fn($v) => !is_null($v) && $v !== '');
|
|
||||||
$average = count($validScores) > 0 ? array_sum($validScores) / count($validScores) : 0;
|
$average = count($validScores) > 0 ? array_sum($validScores) / count($validScores) : 0;
|
||||||
|
|
||||||
// Label nilai untuk tampilan
|
// 2b. Kategorisasi nilai
|
||||||
if ($average >= 85) {
|
if ($average >= 85) {
|
||||||
$katNilai = 'Tinggi';
|
$katNilai = 'Tinggi';
|
||||||
} elseif ($average >= 70) {
|
} elseif ($average >= 70) {
|
||||||
|
|
@ -79,211 +70,326 @@ public function proses(Request $request)
|
||||||
$katNilai = 'Rendah';
|
$katNilai = 'Rendah';
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 2. INPUT SISWA ---
|
// 2c. Skor prestasi
|
||||||
$minatRaw = strtolower(trim($request->minat ?? ''));
|
$prestasiScore = $this->hitungSkorPrestasi($prestasiRaw);
|
||||||
$citaRaw = strtolower(trim($request->cita_cita ?? ''));
|
|
||||||
$prefStudi = $request->pref_studi ?? 'Sains & Teknologi';
|
|
||||||
$prestasiRaw = strtolower(trim($request->prestasi ?? ''));
|
|
||||||
$prestasiScore = $this->scorePrestasiScore($prestasiRaw);
|
|
||||||
|
|
||||||
// --- 3. GRADUATED SCORING PER JURUSAN ---
|
// ================================================================
|
||||||
|
// LANGKAH 3: TENTUKAN HIPOTESIS (H)
|
||||||
|
// H = {Jurusan1, Jurusan2, ..., JurusanN} dari database
|
||||||
|
// ================================================================
|
||||||
$jurusanList = PolijeMajor::all();
|
$jurusanList = PolijeMajor::all();
|
||||||
$hasilAkhir = [];
|
|
||||||
|
|
||||||
// Bobot kriteria
|
if ($jurusanList->isEmpty()) {
|
||||||
$W_NILAI = 0.40;
|
return back()->with('error', 'Data jurusan belum tersedia di database.');
|
||||||
$W_MINAT = 0.35;
|
}
|
||||||
$W_PREF = 0.15;
|
|
||||||
$W_CITA = 0.05;
|
$jumlahJurusan = $jurusanList->count();
|
||||||
$W_PRESTASI = 0.05;
|
|
||||||
|
// ================================================================
|
||||||
|
// LANGKAH 4: HITUNG PROBABILITAS AWAL (PRIOR) P(H)
|
||||||
|
// Prior uniform: P(H) = 1 / jumlah_jurusan
|
||||||
|
// ================================================================
|
||||||
|
$prior = 1 / $jumlahJurusan;
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// LANGKAH 5 & 6: HITUNG LIKELIHOOD DAN PROBABILITAS GABUNGAN
|
||||||
|
// Rumus Naive Bayes:
|
||||||
|
// P(H|X) ∝ P(H) × P(X1|H) × P(X2|H) × P(X3|H) × P(X4|H) × P(X5|H)
|
||||||
|
//
|
||||||
|
// Fitur (Xi):
|
||||||
|
// X1 = Nilai Akademik → P(nilai|H)
|
||||||
|
// X2 = Minat → P(minat|H)
|
||||||
|
// X3 = Preferensi Studi → P(pref|H)
|
||||||
|
// X4 = Cita-cita → P(cita|H)
|
||||||
|
// X5 = Prestasi → P(prestasi|H)
|
||||||
|
//
|
||||||
|
// Weighted Naive Bayes (log-space):
|
||||||
|
// log P(H|X) = log P(H) + Σ wi × log P(Xi|H)
|
||||||
|
//
|
||||||
|
// Bobot (wi):
|
||||||
|
// w1 = 0.40 (Nilai), w2 = 0.35 (Minat), w3 = 0.15 (Pref),
|
||||||
|
// w4 = 0.05 (Cita-cita), w5 = 0.05 (Prestasi)
|
||||||
|
// ================================================================
|
||||||
|
$weights = [
|
||||||
|
'nilai' => 0.40,
|
||||||
|
'minat' => 0.35,
|
||||||
|
'pref' => 0.15,
|
||||||
|
'cita' => 0.05,
|
||||||
|
'prestasi' => 0.05,
|
||||||
|
];
|
||||||
|
|
||||||
|
$logPosteriors = [];
|
||||||
|
$detailPerJurusan = [];
|
||||||
|
|
||||||
foreach ($jurusanList as $jurusan) {
|
foreach ($jurusanList as $jurusan) {
|
||||||
$keywords = $jurusan->keywords ?? [];
|
// --- Log Prior ---
|
||||||
$prefList = $jurusan->preferensi_studi ?? [];
|
$logPrior = log(max($prior, $epsilon));
|
||||||
$bobotMapel = $jurusan->bobot_mapel ?? [];
|
|
||||||
|
|
||||||
// --- Skor Nilai: per-jurusan weighted ---
|
// --- X1: Likelihood Nilai Akademik P(nilai|H) ---
|
||||||
$skorNilai = $this->hitungSkorNilaiPerJurusan($scores, $bobotMapel, $average);
|
$pNilai = $this->hitungLikelihoodNilai($scores, $jurusan->bobot_mapel);
|
||||||
|
|
||||||
// --- Skor Minat: partial keyword matching ---
|
// --- X2: Likelihood Minat P(minat|H) ---
|
||||||
$skorMinat = $this->hitungKecocokanKeyword($minatRaw, $keywords);
|
$pMinat = $this->hitungLikelihoodMinat($minatRaw, $jurusan->keywords);
|
||||||
|
|
||||||
// --- Skor Cita-cita: partial keyword matching ---
|
// --- X3: Likelihood Preferensi Studi P(pref|H) ---
|
||||||
$skorCita = $this->hitungKecocokanKeyword($citaRaw, $keywords);
|
$pPref = $this->hitungLikelihoodPref($prefStudi, $jurusan->preferensi_studi);
|
||||||
|
|
||||||
// --- Skor Preferensi Studi ---
|
// --- X4: Likelihood Cita-cita P(cita|H) ---
|
||||||
if (in_array($prefStudi, $prefList)) {
|
$pCita = $this->hitungLikelihoodCitaCita($citaRaw, $jurusan->keywords);
|
||||||
$skorPref = 1.0;
|
|
||||||
} elseif (!empty($prefList)) {
|
|
||||||
$skorPref = 0.3; // Tidak cocok tapi jurusan punya preferensi
|
|
||||||
} else {
|
|
||||||
$skorPref = 0.5; // Jurusan tidak mendefinisikan preferensi
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Skor Prestasi (sama untuk semua jurusan) ---
|
// --- X5: Likelihood Prestasi P(prestasi|H) ---
|
||||||
$skorPrestasi = $prestasiScore;
|
$pPrestasi = $this->hitungLikelihoodPrestasi($prestasiScore);
|
||||||
|
|
||||||
// --- Hitung skor akhir ---
|
// --- Probabilitas Gabungan (Weighted Naive Bayes) ---
|
||||||
$skorAkhir = ($W_NILAI * $skorNilai) +
|
// log P(H|X) = log P(H) + w1·log P(X1|H) + w2·log P(X2|H) + ... + w5·log P(X5|H)
|
||||||
($W_MINAT * $skorMinat) +
|
$logPosterior = $logPrior
|
||||||
($W_PREF * $skorPref) +
|
+ $weights['nilai'] * log(max($pNilai, $epsilon))
|
||||||
($W_CITA * $skorCita) +
|
+ $weights['minat'] * log(max($pMinat, $epsilon))
|
||||||
($W_PRESTASI * $skorPrestasi);
|
+ $weights['pref'] * log(max($pPref, $epsilon))
|
||||||
|
+ $weights['cita'] * log(max($pCita, $epsilon))
|
||||||
|
+ $weights['prestasi'] * log(max($pPrestasi, $epsilon));
|
||||||
|
|
||||||
$hasilAkhir[] = [
|
$logPosteriors[$jurusan->nama_jurusan] = $logPosterior;
|
||||||
'jurusan' => $jurusan->nama_jurusan,
|
|
||||||
'skor' => round($skorAkhir, 4),
|
// Simpan detail per kriteria untuk tampilan
|
||||||
'detail' => [
|
$detailPerJurusan[$jurusan->nama_jurusan] = [
|
||||||
'nilai' => round($skorNilai, 4),
|
'nilai' => round($pNilai, 4),
|
||||||
'minat' => round($skorMinat, 4),
|
'minat' => round($pMinat, 4),
|
||||||
'pref' => round($skorPref, 4),
|
'pref' => round($pPref, 4),
|
||||||
'cita' => round($skorCita, 4),
|
'cita' => round($pCita, 4),
|
||||||
'prestasi' => round($skorPrestasi, 4),
|
'prestasi' => round($pPrestasi, 4),
|
||||||
],
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort berdasarkan skor tertinggi
|
// ================================================================
|
||||||
|
// LANGKAH 7: KLASIFIKASI (HASIL REKOMENDASI)
|
||||||
|
// Konversi log-posterior ke probabilitas menggunakan softmax
|
||||||
|
// P(Hk|X) = exp(log Pk) / Σ exp(log Pi)
|
||||||
|
// ================================================================
|
||||||
|
$maxLog = max($logPosteriors);
|
||||||
|
$expVals = [];
|
||||||
|
$sumExp = 0.0;
|
||||||
|
|
||||||
|
foreach ($logPosteriors as $namaJurusan => $lv) {
|
||||||
|
$expVals[$namaJurusan] = exp($lv - $maxLog);
|
||||||
|
$sumExp += $expVals[$namaJurusan];
|
||||||
|
}
|
||||||
|
|
||||||
|
$hasilAkhir = [];
|
||||||
|
foreach ($expVals as $namaJurusan => $val) {
|
||||||
|
$prob = $val / max($sumExp, $epsilon);
|
||||||
|
$hasilAkhir[] = [
|
||||||
|
'jurusan' => $namaJurusan,
|
||||||
|
'skor' => round($prob, 4),
|
||||||
|
'detail' => $detailPerJurusan[$namaJurusan],
|
||||||
|
'kecocokan_nilai' => $katNilai,
|
||||||
|
'kecocokan_minat' => $minatRaw,
|
||||||
|
'kecocokan_pref' => $prefStudi,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Urutkan berdasarkan skor tertinggi
|
||||||
usort($hasilAkhir, fn($a, $b) => $b['skor'] <=> $a['skor']);
|
usort($hasilAkhir, fn($a, $b) => $b['skor'] <=> $a['skor']);
|
||||||
|
|
||||||
|
// Ambil data jurusan teratas untuk detail view
|
||||||
|
$topJurusan = !empty($hasilAkhir) ? PolijeMajor::where('nama_jurusan', $hasilAkhir[0]['jurusan'] ?? '')->first() : null;
|
||||||
|
|
||||||
// Simpan ke database
|
// Simpan ke database
|
||||||
|
$user = Auth::user();
|
||||||
|
$savedRec = null;
|
||||||
if ($user) {
|
if ($user) {
|
||||||
Recommendation::create([
|
$savedRec = Recommendation::create([
|
||||||
'user_id' => $user->id,
|
'user_id' => $user->id,
|
||||||
'mtk' => $request->mtk ?? null,
|
'mtk' => $request->mtk ?? null,
|
||||||
'fisika' => $request->fisika ?? null,
|
'fisika' => $request->fisika ?? null,
|
||||||
'kimia' => $request->kimia ?? null,
|
'kimia' => $request->kimia ?? null,
|
||||||
'biologi' => $request->biologi ?? null,
|
'biologi' => $request->biologi ?? null,
|
||||||
'ekonomi' => $request->ekonomi ?? null,
|
'ekonomi' => $request->ekonomi ?? null,
|
||||||
'geografi' => $request->geografi ?? null,
|
'geografi' => $request->geografi ?? null,
|
||||||
'sosiologi' => $request->sosiologi ?? null,
|
'sosiologi' => $request->sosiologi ?? null,
|
||||||
'sejarah' => $request->sejarah ?? null,
|
'sejarah' => $request->sejarah ?? null,
|
||||||
'minat' => $request->minat ?? null,
|
'minat' => $request->minat ?? null,
|
||||||
'preferensi_studi' => $request->pref_studi ?? null,
|
'preferensi_studi' => $request->pref_studi ?? null,
|
||||||
'cita_cita' => $request->cita_cita ?? null,
|
'cita_cita' => $request->cita_cita ?? null,
|
||||||
'prestasi' => $request->prestasi ?? null,
|
'prestasi' => $request->prestasi ?? null,
|
||||||
'hasil_rekomendasi' => $hasilAkhir,
|
'hasil_rekomendasi' => $hasilAkhir,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Simpan recommendation_id ke session agar bisa dipakai link chatbot
|
||||||
|
$recId = $savedRec ? $savedRec->id : null;
|
||||||
|
session(['last_recommendation_id' => $recId]);
|
||||||
|
|
||||||
// Simpan ke session untuk chatbot
|
// Simpan ke session untuk chatbot
|
||||||
if (count($hasilAkhir) > 0) {
|
if (count($hasilAkhir) > 0) {
|
||||||
$topResult = $hasilAkhir[0];
|
$topResult = $hasilAkhir[0];
|
||||||
|
// Ambil top 3 untuk konteks chatbot
|
||||||
|
$top3 = array_slice($hasilAkhir, 0, 3);
|
||||||
session([
|
session([
|
||||||
'recomendation_data' => [
|
'recomendation_data' => [
|
||||||
'jurusan' => $topResult['jurusan'],
|
'jurusan' => $topResult['jurusan'],
|
||||||
'skor' => $topResult['skor'],
|
'skor' => $topResult['skor'], // Sudah 0-1, jangan ×100
|
||||||
'nilai' => $katNilai,
|
'detail' => $topResult['detail'] ?? [],
|
||||||
'minat' => $request->minat,
|
'nilai' => $katNilai,
|
||||||
|
'rata_rata' => round($average, 1),
|
||||||
|
'minat' => $minatRaw,
|
||||||
'pref_studi' => $prefStudi,
|
'pref_studi' => $prefStudi,
|
||||||
|
'cita_cita' => $citaRaw,
|
||||||
|
'prestasi' => $prestasiRaw,
|
||||||
|
'top3' => array_map(fn($r) => [
|
||||||
|
'jurusan' => $r['jurusan'],
|
||||||
|
'skor' => $r['skor'],
|
||||||
|
], $top3),
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load top jurusan from DB for deskripsi & prospek_kerja
|
return view('rekomendasi.hasil', compact(
|
||||||
$topJurusan = null;
|
'hasilAkhir', 'katNilai', 'average', 'prefStudi', 'prestasiScore', 'topJurusan'
|
||||||
if (count($hasilAkhir) > 0) {
|
));
|
||||||
$topJurusan = PolijeMajor::where('nama_jurusan', $hasilAkhir[0]['jurusan'])->first();
|
|
||||||
}
|
|
||||||
|
|
||||||
return view('rekomendasi.hasil', compact('hasilAkhir', 'katNilai', 'average', 'prefStudi', 'prestasiScore', 'topJurusan'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================================================================
|
||||||
|
// FUNGSI LIKELIHOOD — P(Xi | H)
|
||||||
|
// ==================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hitung skor nilai akademik per jurusan dengan bobot mapel
|
* P(nilai | H) — Likelihood nilai akademik terhadap jurusan
|
||||||
* Jika jurusan punya bobot_mapel, hitung weighted average
|
* Menggunakan bobot_mapel dari database untuk menghitung
|
||||||
* Jika tidak, gunakan rata-rata biasa
|
* weighted average yang dinormalisasi ke range probabilitas.
|
||||||
*/
|
*/
|
||||||
private function hitungSkorNilaiPerJurusan(array $scores, array $bobotMapel, float $averageFallback): float
|
private function hitungLikelihoodNilai(array $scores, ?array $bobotMapel): float
|
||||||
{
|
{
|
||||||
// Jika tidak ada bobot khusus, pakai rata-rata biasa
|
// Jika tidak ada bobot, gunakan rata-rata biasa
|
||||||
if (empty($bobotMapel)) {
|
if (empty($bobotMapel)) {
|
||||||
return min($averageFallback / 100, 1.0);
|
$valid = array_filter($scores, fn($v) => $v !== null && $v !== '');
|
||||||
|
if (empty($valid)) return 0.3;
|
||||||
|
$avg = array_sum($valid) / count($valid);
|
||||||
|
return $this->normalisasiProbabilitas($avg / 100, 0.10, 0.95);
|
||||||
}
|
}
|
||||||
|
|
||||||
$weightedSum = 0;
|
$weightedSum = 0;
|
||||||
$totalWeight = 0;
|
$totalWeight = 0;
|
||||||
|
|
||||||
foreach ($bobotMapel as $mapel => $bobot) {
|
foreach ($bobotMapel as $subject => $weight) {
|
||||||
$nilai = floatval($scores[$mapel] ?? 0);
|
$nilai = floatval($scores[$subject] ?? 0);
|
||||||
$weightedSum += $nilai * $bobot;
|
if ($nilai > 0 && $weight > 0) {
|
||||||
$totalWeight += $bobot;
|
$weightedSum += $weight * ($nilai / 100);
|
||||||
}
|
$totalWeight += $weight;
|
||||||
|
|
||||||
// Untuk mapel yang ada di scores tapi tidak di bobot, beri bobot kecil
|
|
||||||
foreach ($scores as $mapel => $nilai) {
|
|
||||||
if (!isset($bobotMapel[$mapel]) && !is_null($nilai) && $nilai !== '') {
|
|
||||||
$weightedSum += floatval($nilai) * 0.1;
|
|
||||||
$totalWeight += 0.1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($totalWeight <= 0) {
|
if ($totalWeight == 0) return 0.3;
|
||||||
return min($averageFallback / 100, 1.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
$weightedAvg = $weightedSum / $totalWeight;
|
$weightedAvg = $weightedSum / $totalWeight;
|
||||||
return min($weightedAvg / 100, 1.0);
|
return $this->normalisasiProbabilitas($weightedAvg, 0.10, 0.95);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hitung kecocokan teks input dengan array keywords jurusan (graduated)
|
* P(minat | H) — Likelihood minat terhadap jurusan
|
||||||
* Returns 0.0 - 1.0
|
* Menggunakan keyword matching terhadap keywords jurusan dari database.
|
||||||
*/
|
*/
|
||||||
private function hitungKecocokanKeyword(string $inputText, array $keywords): float
|
private function hitungLikelihoodMinat(string $minatRaw, ?array $keywords): float
|
||||||
{
|
{
|
||||||
if (empty($keywords) || empty($inputText)) {
|
if (empty($keywords) || empty($minatRaw)) {
|
||||||
return 0.0;
|
return 0.20; // probabilitas dasar jika tidak ada data
|
||||||
}
|
}
|
||||||
|
|
||||||
$matchCount = 0;
|
$matchCount = 0;
|
||||||
$inputWords = preg_split('/[\s,;.\/\-]+/', $inputText);
|
|
||||||
|
|
||||||
foreach ($keywords as $keyword) {
|
foreach ($keywords as $keyword) {
|
||||||
$kw = strtolower(trim($keyword));
|
if (stripos($minatRaw, strtolower($keyword)) !== false) {
|
||||||
if (empty($kw)) continue;
|
$matchCount++;
|
||||||
|
|
||||||
// Check if keyword appears in any input word (partial match)
|
|
||||||
foreach ($inputWords as $word) {
|
|
||||||
if (empty($word)) continue;
|
|
||||||
// Match if input word contains keyword or keyword contains input word (min 3 chars)
|
|
||||||
if (stripos($inputText, $kw) !== false ||
|
|
||||||
(strlen($word) >= 3 && stripos($kw, $word) !== false)) {
|
|
||||||
$matchCount++;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Graduated score: ratio of matched keywords
|
// Rasio kecocokan keyword
|
||||||
// Use sqrt to give more credit for partial matches
|
$matchRatio = $matchCount / count($keywords);
|
||||||
$ratio = $matchCount / count($keywords);
|
|
||||||
return min(sqrt($ratio) * 0.9 + ($matchCount > 0 ? 0.1 : 0), 1.0);
|
// Konversi ke range probabilitas: 0 match → 0.10, full match → 0.95
|
||||||
|
return $this->normalisasiProbabilitas($matchRatio, 0.10, 0.95);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scoring prestasi berdasarkan keyword
|
* P(pref | H) — Likelihood preferensi studi terhadap jurusan
|
||||||
|
* Membandingkan preferensi siswa dengan preferensi_studi jurusan dari database.
|
||||||
*/
|
*/
|
||||||
private function scorePrestasiScore(string $prestasiRaw): float
|
private function hitungLikelihoodPref(string $prefStudi, ?array $jurusanPref): float
|
||||||
|
{
|
||||||
|
if (empty($jurusanPref)) {
|
||||||
|
return 0.40; // probabilitas netral
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cek apakah preferensi siswa ada di list preferensi jurusan
|
||||||
|
if (in_array($prefStudi, $jurusanPref)) {
|
||||||
|
return 0.85; // cocok
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0.15; // tidak cocok
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* P(cita_cita | H) — Likelihood cita-cita terhadap jurusan
|
||||||
|
* Menggunakan keyword matching dari cita-cita siswa terhadap keywords jurusan.
|
||||||
|
*/
|
||||||
|
private function hitungLikelihoodCitaCita(string $citaRaw, ?array $keywords): float
|
||||||
|
{
|
||||||
|
if (empty($keywords) || empty($citaRaw)) {
|
||||||
|
return 0.25; // probabilitas dasar
|
||||||
|
}
|
||||||
|
|
||||||
|
$matchCount = 0;
|
||||||
|
foreach ($keywords as $keyword) {
|
||||||
|
if (stripos($citaRaw, strtolower($keyword)) !== false) {
|
||||||
|
$matchCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$matchRatio = $matchCount / count($keywords);
|
||||||
|
return $this->normalisasiProbabilitas($matchRatio, 0.10, 0.90);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* P(prestasi | H) — Likelihood prestasi
|
||||||
|
* Prestasi bersifat umum (tidak spesifik per jurusan), sehingga
|
||||||
|
* memberikan boost yang sama untuk semua jurusan.
|
||||||
|
*/
|
||||||
|
private function hitungLikelihoodPrestasi(float $prestasiScore): float
|
||||||
|
{
|
||||||
|
// Konversi skor prestasi (0-1) ke range probabilitas
|
||||||
|
return $this->normalisasiProbabilitas($prestasiScore, 0.20, 0.90);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================================================================
|
||||||
|
// FUNGSI HELPER
|
||||||
|
// ==================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalisasi nilai (0-1) ke range probabilitas [min, max]
|
||||||
|
* Agar tidak ada likelihood 0 atau 1 (menghindari dominasi)
|
||||||
|
*/
|
||||||
|
private function normalisasiProbabilitas(float $value, float $min = 0.10, float $max = 0.95): float
|
||||||
|
{
|
||||||
|
return $min + ($value * ($max - $min));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hitung skor prestasi berdasarkan keyword
|
||||||
|
*/
|
||||||
|
private function hitungSkorPrestasi(string $prestasiRaw): float
|
||||||
{
|
{
|
||||||
if (empty($prestasiRaw)) {
|
if (empty($prestasiRaw)) {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
$prestasiScore = 0.0;
|
|
||||||
|
|
||||||
// Berbagai tingkat prestasi
|
|
||||||
if (preg_match('/(juara|menang|champion|first|gold|emas|terbaik)/', $prestasiRaw)) {
|
if (preg_match('/(juara|menang|champion|first|gold|emas|terbaik)/', $prestasiRaw)) {
|
||||||
$prestasiScore = 0.90; // Prestasi tinggi
|
return 0.90;
|
||||||
} elseif (preg_match('/(finalis|semifinal|peringkat|ranking|podium|medali|silver|silver|perak)/', $prestasiRaw)) {
|
} elseif (preg_match('/(finalis|semifinal|peringkat|ranking|podium|medali|silver|perak)/', $prestasiRaw)) {
|
||||||
$prestasiScore = 0.75; // Prestasi sedang
|
return 0.75;
|
||||||
} elseif (preg_match('/(sertifikat|training|kursus|workshop|peserta|mengikuti)/', $prestasiRaw)) {
|
} elseif (preg_match('/(sertifikat|training|kursus|workshop|peserta|mengikuti)/', $prestasiRaw)) {
|
||||||
$prestasiScore = 0.60; // Prestasi cukup
|
return 0.60;
|
||||||
} else {
|
|
||||||
$prestasiScore = 0.30; // Prestasi minimal
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $prestasiScore;
|
return 0.30;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@ class ChatHistory extends Model
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'user_id',
|
'user_id',
|
||||||
|
'session_id',
|
||||||
|
'recommendation_id',
|
||||||
'prompt',
|
'prompt',
|
||||||
'response',
|
'response',
|
||||||
];
|
];
|
||||||
|
|
@ -21,4 +23,9 @@ public function user()
|
||||||
{
|
{
|
||||||
return $this->belongsTo(User::class);
|
return $this->belongsTo(User::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function recommendation()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Recommendation::class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,17 @@ protected function getFallbackResponse($message, $context = [])
|
||||||
$messageLower = strtolower($message);
|
$messageLower = strtolower($message);
|
||||||
|
|
||||||
if (strpos($messageLower, 'halo') !== false || strpos($messageLower, 'hai') !== false || strpos($messageLower, 'hallo') !== false || strpos($messageLower, 'hi') !== false) {
|
if (strpos($messageLower, 'halo') !== false || strpos($messageLower, 'hai') !== false || strpos($messageLower, 'hallo') !== false || strpos($messageLower, 'hi') !== false) {
|
||||||
$greeting = "Selamat datang. Saya adalah konselor BK virtual SMA Bima Ambulu yang siap membantu Anda dalam pemilihan jurusan kuliah. ";
|
$hour = (int) now()->format('H');
|
||||||
|
if ($hour >= 3 && $hour < 11) {
|
||||||
|
$sapaan = 'Selamat pagi';
|
||||||
|
} elseif ($hour >= 11 && $hour < 15) {
|
||||||
|
$sapaan = 'Selamat siang';
|
||||||
|
} elseif ($hour >= 15 && $hour < 18) {
|
||||||
|
$sapaan = 'Selamat sore';
|
||||||
|
} else {
|
||||||
|
$sapaan = 'Selamat malam';
|
||||||
|
}
|
||||||
|
$greeting = "{$sapaan}. Saya adalah konselor BK virtual SMA Bima Ambulu yang siap membantu Anda dalam pemilihan jurusan kuliah. ";
|
||||||
if ($hasRecommendation) {
|
if ($hasRecommendation) {
|
||||||
$greeting .= "Berdasarkan data yang tersedia, Anda telah memperoleh rekomendasi jurusan \"{$jurusan}\" dengan skor kesesuaian {$score}%. Apakah Anda ingin membahas lebih lanjut mengenai jurusan tersebut, atau ada pertanyaan lain yang ingin disampaikan?";
|
$greeting .= "Berdasarkan data yang tersedia, Anda telah memperoleh rekomendasi jurusan \"{$jurusan}\" dengan skor kesesuaian {$score}%. Apakah Anda ingin membahas lebih lanjut mengenai jurusan tersebut, atau ada pertanyaan lain yang ingin disampaikan?";
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -151,7 +161,7 @@ protected function getFallbackResponse($message, $context = [])
|
||||||
if ($hasRecommendation) {
|
if ($hasRecommendation) {
|
||||||
return [
|
return [
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => "Jurusan \"{$jurusan}\" memiliki prospek karier yang menjanjikan. Lulusan dari jurusan ini dapat bekerja di berbagai sektor industri yang relevan dengan bidang keahliannya. Setiap program studi di perguruan tinggi dirancang untuk membekali lulusannya dengan kompetensi praktis yang dibutuhkan oleh dunia kerja. Apakah Anda ingin mengetahui lebih detail mengenai posisi pekerjaan spesifik yang dapat ditempuh?"
|
'message' => "Jurusan \"{$jurusan}\" memiliki prospek karier yang menjanjikan. Lulusan dari jurusan ini dapat bekerja di berbagai sektor industri yang relevan dengan bidang keahliannya. Setiap jurusan di POLIJE dirancang untuk membekali lulusannya dengan kompetensi praktis yang dibutuhkan oleh dunia kerja. Apakah Anda ingin mengetahui lebih detail mengenai posisi pekerjaan spesifik yang dapat ditempuh?"
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
|
|
@ -183,7 +193,7 @@ protected function getFallbackResponse($message, $context = [])
|
||||||
if (strpos($messageLower, 'ipa') !== false || strpos($messageLower, 'ips') !== false) {
|
if (strpos($messageLower, 'ipa') !== false || strpos($messageLower, 'ips') !== false) {
|
||||||
return [
|
return [
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => "Perlu dipahami bahwa kelompok IPA dan IPS bukan merupakan batasan mutlak dalam memilih jurusan kuliah. Banyak program studi yang dapat dimasuki oleh siswa dari kedua kelompok tersebut. Faktor yang lebih menentukan adalah minat, kemampuan, dan kompetensi yang Anda miliki. Siswa IPA dapat memilih bidang bisnis, dan sebaliknya siswa IPS dapat menempuh bidang teknologi informasi. Silakan manfaatkan fitur Analisis Rekomendasi untuk melihat jurusan yang paling sesuai berdasarkan profil lengkap Anda."
|
'message' => "Perlu dipahami bahwa kelompok IPA dan IPS bukan merupakan batasan mutlak dalam memilih jurusan kuliah. Banyak jurusan di POLIJE dapat dimasuki oleh siswa dari kedua kelompok tersebut. Faktor yang lebih menentukan adalah minat, kemampuan, dan kompetensi yang Anda miliki. Siswa IPA dapat memilih bidang bisnis, dan sebaliknya siswa IPS dapat menempuh bidang teknologi informasi. Silakan manfaatkan fitur Analisis Rekomendasi untuk melihat jurusan yang paling sesuai berdasarkan profil lengkap Anda."
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -210,13 +220,53 @@ protected function buildSystemPrompt($context)
|
||||||
$prompt .= "Gunakan bahasa Indonesia yang FORMAL, AKADEMIK, dan SOPAN — seperti seorang konselor profesional berbicara dengan siswa. ";
|
$prompt .= "Gunakan bahasa Indonesia yang FORMAL, AKADEMIK, dan SOPAN — seperti seorang konselor profesional berbicara dengan siswa. ";
|
||||||
$prompt .= "DILARANG menggunakan bahasa gaul, slang, atau terlalu santai. Gunakan kalimat yang baku dan terstruktur. ";
|
$prompt .= "DILARANG menggunakan bahasa gaul, slang, atau terlalu santai. Gunakan kalimat yang baku dan terstruktur. ";
|
||||||
|
|
||||||
|
// PENTING: Konteks POLIJE
|
||||||
|
$prompt .= "\n\nKONTEKS PENTING POLIJE (Politeknik Negeri Jember):";
|
||||||
|
$prompt .= "\n- Di POLIJE, unit akademik disebut JURUSAN, BUKAN 'program studi' atau 'prodi'. Selalu gunakan istilah 'jurusan'.";
|
||||||
|
$prompt .= "\n- Contoh benar: 'Jurusan Teknologi Informasi', 'Jurusan Kesehatan', 'Jurusan Manajemen Agribisnis'.";
|
||||||
|
$prompt .= "\n- Contoh SALAH (JANGAN digunakan): 'program studi Teknologi Informasi', 'prodi TI'.";
|
||||||
|
$prompt .= "\n- Saat menjelaskan jurusan POLIJE, PRIORITASKAN data dari DAFTAR JURUSAN di bawah (deskripsi, prospek kerja, kata kunci).";
|
||||||
|
$prompt .= "\n- JANGAN gunakan informasi umum dari internet yang bisa berbeda dengan konteks POLIJE.";
|
||||||
|
$prompt .= "\n- Jika siswa bertanya tentang suatu jurusan, jawab berdasarkan data POLIJE yang tersedia, bukan pengetahuan umum.";
|
||||||
|
|
||||||
|
// Tambahkan informasi waktu saat ini
|
||||||
|
$hour = (int) now()->format('H');
|
||||||
|
if ($hour >= 3 && $hour < 11) {
|
||||||
|
$sapaan = 'Selamat pagi';
|
||||||
|
$waktu = 'pagi';
|
||||||
|
} elseif ($hour >= 11 && $hour < 15) {
|
||||||
|
$sapaan = 'Selamat siang';
|
||||||
|
$waktu = 'siang';
|
||||||
|
} elseif ($hour >= 15 && $hour < 18) {
|
||||||
|
$sapaan = 'Selamat sore';
|
||||||
|
$waktu = 'sore';
|
||||||
|
} else {
|
||||||
|
$sapaan = 'Selamat malam';
|
||||||
|
$waktu = 'malam';
|
||||||
|
}
|
||||||
|
$prompt .= "\n\nWAKTU SAAT INI: " . now()->format('H:i') . " WIB (" . $waktu . "). ";
|
||||||
|
$prompt .= "Jika perlu menyapa, gunakan sapaan yang sesuai waktu: '{$sapaan}'. JANGAN gunakan sapaan waktu yang tidak sesuai (misalnya jangan ucapkan 'Selamat pagi' jika saat ini malam hari).";
|
||||||
|
|
||||||
// Tambahkan konteks rekomendasi jika ada
|
// Tambahkan konteks rekomendasi jika ada
|
||||||
if (!empty($context['recommendation'])) {
|
if (!empty($context['recommendation'])) {
|
||||||
$prompt .= "\n\nDATA REKOMENDASI SISWA (dari sistem analisis): ";
|
$prompt .= "\n\nDATA REKOMENDASI SISWA (dari sistem analisis Naive Bayes): ";
|
||||||
$prompt .= "Jurusan paling cocok: {$context['recommendation']}. ";
|
$prompt .= "Jurusan paling cocok: {$context['recommendation']}. ";
|
||||||
if (!empty($context['score'])) {
|
if (!empty($context['score'])) {
|
||||||
$prompt .= "Skor kesesuaian: {$context['score']}%. ";
|
$prompt .= "Skor kesesuaian: {$context['score']}%. ";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tambahkan top 3 rekomendasi
|
||||||
|
if (!empty($context['top3'])) {
|
||||||
|
$prompt .= "\nPeringkat 3 besar rekomendasi: ";
|
||||||
|
foreach ($context['top3'] as $i => $t) {
|
||||||
|
$num = $i + 1;
|
||||||
|
$skorVal = $t['skor'] ?? 0;
|
||||||
|
$pct = number_format(($skorVal > 1 ? $skorVal : $skorVal * 100), 1);
|
||||||
|
$prompt .= "\n {$num}. {$t['jurusan']} ({$pct}%)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$prompt .= "\nGunakan data rekomendasi ini untuk menjelaskan MENGAPA jurusan tersebut direkomendasikan. Hubungkan dengan profil siswa di bawah.";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tambahkan profil siswa jika ada
|
// Tambahkan profil siswa jika ada
|
||||||
|
|
@ -229,29 +279,41 @@ protected function buildSystemPrompt($context)
|
||||||
$prompt .= "Kelompok asal: {$context['profile']['kelompok']}. ";
|
$prompt .= "Kelompok asal: {$context['profile']['kelompok']}. ";
|
||||||
}
|
}
|
||||||
if (!empty($context['profile']['nilai'])) {
|
if (!empty($context['profile']['nilai'])) {
|
||||||
$prompt .= "Nilai akademik: {$context['profile']['nilai']}. ";
|
$prompt .= "Kategori nilai akademik: {$context['profile']['nilai']}. ";
|
||||||
|
}
|
||||||
|
if (!empty($context['profile']['rata_rata'])) {
|
||||||
|
$prompt .= "Rata-rata nilai: {$context['profile']['rata_rata']}. ";
|
||||||
}
|
}
|
||||||
if (!empty($context['profile']['minat'])) {
|
if (!empty($context['profile']['minat'])) {
|
||||||
$prompt .= "Minat: {$context['profile']['minat']}. ";
|
$prompt .= "Minat: {$context['profile']['minat']}. ";
|
||||||
}
|
}
|
||||||
if (!empty($context['profile']['pref'])) {
|
if (!empty($context['profile']['pref'])) {
|
||||||
$prompt .= "Preferensi pembelajaran: {$context['profile']['pref']}. ";
|
$prompt .= "Preferensi rumpun studi: {$context['profile']['pref']}. ";
|
||||||
|
}
|
||||||
|
if (!empty($context['profile']['cita_cita'])) {
|
||||||
|
$prompt .= "Cita-cita: {$context['profile']['cita_cita']}. ";
|
||||||
|
}
|
||||||
|
if (!empty($context['profile']['prestasi'])) {
|
||||||
|
$prompt .= "Prestasi: {$context['profile']['prestasi']}. ";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$jurusanList = PolijeMajor::all();
|
$jurusanList = PolijeMajor::all();
|
||||||
if ($jurusanList->isNotEmpty()) {
|
if ($jurusanList->isNotEmpty()) {
|
||||||
$prompt .= "\n\nDAFTAR JURUSAN POLIJE ({$jurusanList->count()} jurusan):";
|
$prompt .= "\n\nDAFTAR JURUSAN POLIJE ({$jurusanList->count()} jurusan) — INI ADALAH SUMBER DATA UTAMA, gunakan informasi ini saat menjelaskan jurusan:";
|
||||||
foreach ($jurusanList as $j) {
|
foreach ($jurusanList as $j) {
|
||||||
$prompt .= "\n- {$j->nama_jurusan}";
|
$prompt .= "\n- JURUSAN {$j->nama_jurusan}";
|
||||||
if (!empty($j->deskripsi)) {
|
if (!empty($j->deskripsi)) {
|
||||||
$prompt .= ": {$j->deskripsi}";
|
$prompt .= ": {$j->deskripsi}";
|
||||||
}
|
}
|
||||||
if (!empty($j->prospek_kerja)) {
|
if (!empty($j->prospek_kerja)) {
|
||||||
$prompt .= " Prospek kerja: {$j->prospek_kerja}.";
|
$prompt .= " | Prospek kerja: {$j->prospek_kerja}.";
|
||||||
}
|
}
|
||||||
if (!empty($j->keywords) && is_array($j->keywords)) {
|
if (!empty($j->keywords) && is_array($j->keywords)) {
|
||||||
$prompt .= " Kata kunci: " . implode(', ', array_slice($j->keywords, 0, 10)) . ".";
|
$prompt .= " | Kata kunci: " . implode(', ', array_slice($j->keywords, 0, 10)) . ".";
|
||||||
|
}
|
||||||
|
if (!empty($j->preferensi_studi) && is_array($j->preferensi_studi)) {
|
||||||
|
$prompt .= " | Rumpun: " . implode(', ', $j->preferensi_studi) . ".";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -265,7 +327,17 @@ protected function buildSystemPrompt($context)
|
||||||
$prompt .= "\n6. Jawab RINGKAS (2-3 paragraf). Jangan terlalu panjang kecuali diminta detail.";
|
$prompt .= "\n6. Jawab RINGKAS (2-3 paragraf). Jangan terlalu panjang kecuali diminta detail.";
|
||||||
$prompt .= "\n7. Boleh menjawab pertanyaan di luar topik jurusan secara singkat, lalu kembalikan ke konseling.";
|
$prompt .= "\n7. Boleh menjawab pertanyaan di luar topik jurusan secara singkat, lalu kembalikan ke konseling.";
|
||||||
$prompt .= "\n8. JANGAN awali setiap respons dengan 'Halo' atau salam — langsung ke inti jawaban (kecuali percakapan baru dimulai).";
|
$prompt .= "\n8. JANGAN awali setiap respons dengan 'Halo' atau salam — langsung ke inti jawaban (kecuali percakapan baru dimulai).";
|
||||||
$prompt .= "\n9. DILARANG KERAS menggunakan format markdown seperti **, *, #, ##, atau simbol formatting lainnya. Tulis teks biasa (plain text) saja tanpa formatting markdown.";
|
|
||||||
|
// Tambahkan referensi Q&A serupa dari riwayat
|
||||||
|
if (!empty($context['similar_qa'])) {
|
||||||
|
$prompt .= "\n\nREFERENSI JAWABAN SEBELUMNYA (pertanyaan serupa yang pernah dijawab — gunakan sebagai referensi untuk konsistensi, tapi sesuaikan dengan konteks siswa saat ini):";
|
||||||
|
foreach ($context['similar_qa'] as $i => $qa) {
|
||||||
|
$num = $i + 1;
|
||||||
|
$prompt .= "\n{$num}. Pertanyaan: \"{$qa['prompt']}\"";
|
||||||
|
$prompt .= "\n Jawaban sebelumnya: \"{$qa['response']}\"";
|
||||||
|
}
|
||||||
|
$prompt .= "\nGunakan referensi di atas untuk menjaga konsistensi jawaban, namun tetap sesuaikan dengan profil dan konteks percakapan siswa saat ini.";
|
||||||
|
} $prompt .= "\n9. DILARANG KERAS menggunakan format markdown seperti **, *, #, ##, atau simbol formatting lainnya. Tulis teks biasa (plain text) saja tanpa formatting markdown.";
|
||||||
$prompt .= "\n10. Gunakan bahasa Indonesia baku dan akademik. Hindari bahasa gaul seperti 'kek', 'banget', 'ngobrol', 'ngomongin', 'gampangnya'. Gunakan padanan formal seperti 'sangat', 'berbincang', 'membahas', 'secara sederhana'.";
|
$prompt .= "\n10. Gunakan bahasa Indonesia baku dan akademik. Hindari bahasa gaul seperti 'kek', 'banget', 'ngobrol', 'ngomongin', 'gampangnya'. Gunakan padanan formal seperti 'sangat', 'berbincang', 'membahas', 'secara sederhana'.";
|
||||||
|
|
||||||
return $prompt;
|
return $prompt;
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'timezone' => 'UTC',
|
'timezone' => 'Asia/Jakarta',
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('chat_histories', function (Blueprint $table) {
|
||||||
|
$table->string('session_id', 36)->nullable()->after('user_id')->index();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('chat_histories', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('session_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Backfill session_id untuk record lama yang belum punya.
|
||||||
|
* Mengelompokkan berdasarkan user_id + tanggal.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
// Ambil semua record tanpa session_id
|
||||||
|
$records = DB::table('chat_histories')
|
||||||
|
->whereNull('session_id')
|
||||||
|
->orderBy('user_id')
|
||||||
|
->orderBy('created_at')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
// Kelompokkan per user_id + tanggal
|
||||||
|
$groups = $records->groupBy(function ($record) {
|
||||||
|
return $record->user_id . '_' . substr($record->created_at, 0, 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach ($groups as $key => $chats) {
|
||||||
|
$uuid = Str::uuid()->toString();
|
||||||
|
$ids = $chats->pluck('id')->toArray();
|
||||||
|
|
||||||
|
DB::table('chat_histories')
|
||||||
|
->whereIn('id', $ids)
|
||||||
|
->update(['session_id' => $uuid]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
// Tidak bisa di-reverse secara akurat
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('chat_histories', function (Blueprint $table) {
|
||||||
|
$table->unsignedBigInteger('recommendation_id')->nullable()->after('session_id');
|
||||||
|
$table->foreign('recommendation_id')->references('id')->on('recommendations')->onDelete('set null');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Backfill: untuk setiap session, cari rekomendasi terdekat sebelum chat pertama user tersebut
|
||||||
|
$sessions = DB::table('chat_histories')
|
||||||
|
->select('session_id', 'user_id', DB::raw('MIN(created_at) as first_chat_at'))
|
||||||
|
->whereNotNull('session_id')
|
||||||
|
->whereNull('recommendation_id')
|
||||||
|
->groupBy('session_id', 'user_id')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($sessions as $session) {
|
||||||
|
// Cari rekomendasi terakhir user sebelum atau tepat saat sesi dimulai
|
||||||
|
$rec = DB::table('recommendations')
|
||||||
|
->where('user_id', $session->user_id)
|
||||||
|
->where('created_at', '<=', $session->first_chat_at)
|
||||||
|
->orderBy('created_at', 'desc')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($rec) {
|
||||||
|
DB::table('chat_histories')
|
||||||
|
->where('session_id', $session->session_id)
|
||||||
|
->update(['recommendation_id' => $rec->id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('chat_histories', function (Blueprint $table) {
|
||||||
|
$table->dropForeign(['recommendation_id']);
|
||||||
|
$table->dropColumn('recommendation_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -16,10 +16,10 @@ public function run(): void
|
||||||
{
|
{
|
||||||
// Create Admin User
|
// Create Admin User
|
||||||
User::firstOrCreate(
|
User::firstOrCreate(
|
||||||
['email' => 'admin@gmail.com'],
|
['email' => 'admin@polije.ac.id'],
|
||||||
[
|
[
|
||||||
'name' => 'Admin Polije',
|
'name' => 'Admin Polije',
|
||||||
'password' => Hash::make('admin123'),
|
'password' => Hash::make('admin1234'),
|
||||||
'role' => 'admin',
|
'role' => 'admin',
|
||||||
'email_verified_at' => now(),
|
'email_verified_at' => now(),
|
||||||
]
|
]
|
||||||
|
|
@ -27,17 +27,17 @@ public function run(): void
|
||||||
|
|
||||||
// Create BK (Konselor) User
|
// Create BK (Konselor) User
|
||||||
User::firstOrCreate(
|
User::firstOrCreate(
|
||||||
['email' => 'bk@gmail.com'],
|
['email' => 'gurubk@polije.ac.id'],
|
||||||
[
|
[
|
||||||
'name' => 'Konselor BK',
|
'name' => 'Konselor BK',
|
||||||
'password' => Hash::make('bk123'),
|
'password' => Hash::make('gurubk1234'),
|
||||||
'role' => 'bk',
|
'role' => 'bk',
|
||||||
'email_verified_at' => now(),
|
'email_verified_at' => now(),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
echo "✅ Admin & BK users created successfully!\n";
|
echo "✅ Admin & BK users created successfully!\n";
|
||||||
echo "Admin: admin@gmail.com / admin123\n";
|
echo "Admin: admin@polije.ac.id / admin1234\n";
|
||||||
echo "BK: bk@gmail.com / bk123\n";
|
echo "BK: gurubk@polije.ac.id / gurubk1234\n";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ public function run(): void
|
||||||
$jurusans = [
|
$jurusans = [
|
||||||
[
|
[
|
||||||
'nama_jurusan' => 'Produksi Pertanian',
|
'nama_jurusan' => 'Produksi Pertanian',
|
||||||
'deskripsi' => 'Program studi yang mempelajari teknik budidaya tanaman, pengelolaan lahan pertanian, dan produksi hasil pertanian secara modern.',
|
'deskripsi' => 'Jurusan yang mempelajari teknik budidaya tanaman, pengelolaan lahan pertanian, dan produksi hasil pertanian secara modern.',
|
||||||
'keywords' => ['pertanian', 'petani', 'kebun', 'sawah', 'panen', 'tanaman', 'budidaya', 'agronomi', 'tanam', 'bercocok tanam', 'alam', 'hortikultura', 'pupuk', 'bibit'],
|
'keywords' => ['pertanian', 'petani', 'kebun', 'sawah', 'panen', 'tanaman', 'budidaya', 'agronomi', 'tanam', 'bercocok tanam', 'alam', 'hortikultura', 'pupuk', 'bibit'],
|
||||||
'preferensi_studi' => ['Pertanian & Lingkungan'],
|
'preferensi_studi' => ['Pertanian & Lingkungan'],
|
||||||
'bobot_mapel' => [
|
'bobot_mapel' => [
|
||||||
|
|
@ -23,7 +23,7 @@ public function run(): void
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'nama_jurusan' => 'Teknologi Pertanian',
|
'nama_jurusan' => 'Teknologi Pertanian',
|
||||||
'deskripsi' => 'Program studi yang mengintegrasikan teknologi dengan pertanian, meliputi mekanisasi pertanian, pengolahan hasil pertanian, dan inovasi teknologi pangan.',
|
'deskripsi' => 'Jurusan yang mengintegrasikan teknologi dengan pertanian, meliputi mekanisasi pertanian, pengolahan hasil pertanian, dan inovasi teknologi pangan.',
|
||||||
'keywords' => ['teknologi pertanian', 'mesin pertanian', 'inovasi', 'otomasi', 'pengolahan pangan', 'pangan', 'mekanisasi', 'teknologi pangan', 'alat pertanian', 'rekayasa'],
|
'keywords' => ['teknologi pertanian', 'mesin pertanian', 'inovasi', 'otomasi', 'pengolahan pangan', 'pangan', 'mekanisasi', 'teknologi pangan', 'alat pertanian', 'rekayasa'],
|
||||||
'preferensi_studi' => ['Sains & Teknologi', 'Pertanian & Lingkungan'],
|
'preferensi_studi' => ['Sains & Teknologi', 'Pertanian & Lingkungan'],
|
||||||
'bobot_mapel' => [
|
'bobot_mapel' => [
|
||||||
|
|
@ -34,7 +34,7 @@ public function run(): void
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'nama_jurusan' => 'Peternakan',
|
'nama_jurusan' => 'Peternakan',
|
||||||
'deskripsi' => 'Program studi yang mempelajari pengelolaan dan pemeliharaan ternak, nutrisi hewan, reproduksi, dan pengolahan produk peternakan.',
|
'deskripsi' => 'Jurusan yang mempelajari pengelolaan dan pemeliharaan ternak, nutrisi hewan, reproduksi, dan pengolahan produk peternakan.',
|
||||||
'keywords' => ['ternak', 'hewan', 'peternakan', 'peternak', 'sapi', 'ayam', 'unggas', 'kambing', 'susu', 'pakan', 'nutrisi hewan', 'veteriner', 'ikan', 'aquaculture'],
|
'keywords' => ['ternak', 'hewan', 'peternakan', 'peternak', 'sapi', 'ayam', 'unggas', 'kambing', 'susu', 'pakan', 'nutrisi hewan', 'veteriner', 'ikan', 'aquaculture'],
|
||||||
'preferensi_studi' => ['Pertanian & Lingkungan', 'Kesehatan & Ilmu Hayat'],
|
'preferensi_studi' => ['Pertanian & Lingkungan', 'Kesehatan & Ilmu Hayat'],
|
||||||
'bobot_mapel' => [
|
'bobot_mapel' => [
|
||||||
|
|
@ -45,7 +45,7 @@ public function run(): void
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'nama_jurusan' => 'Manajemen Agribisnis',
|
'nama_jurusan' => 'Manajemen Agribisnis',
|
||||||
'deskripsi' => 'Program studi yang menggabungkan ilmu pertanian dan bisnis, meliputi pemasaran hasil pertanian, manajemen usaha tani, dan kewirausahaan agribisnis.',
|
'deskripsi' => 'Jurusan yang menggabungkan ilmu pertanian dan bisnis, meliputi pemasaran hasil pertanian, manajemen usaha tani, dan kewirausahaan agribisnis.',
|
||||||
'keywords' => ['bisnis', 'agribisnis', 'usaha', 'entrepreneur', 'pengusaha', 'dagang', 'jual', 'pemasaran', 'kewirausahaan', 'manajemen', 'ekonomi pertanian', 'pasar'],
|
'keywords' => ['bisnis', 'agribisnis', 'usaha', 'entrepreneur', 'pengusaha', 'dagang', 'jual', 'pemasaran', 'kewirausahaan', 'manajemen', 'ekonomi pertanian', 'pasar'],
|
||||||
'preferensi_studi' => ['Bisnis & Manajemen', 'Pertanian & Lingkungan'],
|
'preferensi_studi' => ['Bisnis & Manajemen', 'Pertanian & Lingkungan'],
|
||||||
'bobot_mapel' => [
|
'bobot_mapel' => [
|
||||||
|
|
@ -56,7 +56,7 @@ public function run(): void
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'nama_jurusan' => 'Teknologi Informasi',
|
'nama_jurusan' => 'Teknologi Informasi',
|
||||||
'deskripsi' => 'Program studi yang mempelajari pengembangan perangkat lunak, jaringan komputer, keamanan siber, dan teknologi digital.',
|
'deskripsi' => 'Jurusan yang mempelajari pengembangan perangkat lunak, jaringan komputer, keamanan siber, dan teknologi digital.',
|
||||||
'keywords' => ['programmer', 'developer', 'coding', 'software', 'web', 'aplikasi', 'komputer', 'it', 'jaringan', 'hacker', 'game', 'data', 'ai', 'robot', 'ngoding', 'laptop', 'teknologi', 'digital', 'internet', 'programming', 'desain grafis'],
|
'keywords' => ['programmer', 'developer', 'coding', 'software', 'web', 'aplikasi', 'komputer', 'it', 'jaringan', 'hacker', 'game', 'data', 'ai', 'robot', 'ngoding', 'laptop', 'teknologi', 'digital', 'internet', 'programming', 'desain grafis'],
|
||||||
'preferensi_studi' => ['Sains & Teknologi'],
|
'preferensi_studi' => ['Sains & Teknologi'],
|
||||||
'bobot_mapel' => [
|
'bobot_mapel' => [
|
||||||
|
|
@ -67,7 +67,7 @@ public function run(): void
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'nama_jurusan' => 'Teknik',
|
'nama_jurusan' => 'Teknik',
|
||||||
'deskripsi' => 'Program studi yang mempelajari mesin, kelistrikan, elektronika, dan otomasi industri.',
|
'deskripsi' => 'Jurusan yang mempelajari mesin, kelistrikan, elektronika, dan otomasi industri.',
|
||||||
'keywords' => ['mesin', 'bengkel', 'listrik', 'las', 'robot', 'motor', 'teknik', 'otomasi', 'elektronik', 'instalasi', 'panel', 'mekanik', 'industri', 'manufaktur', 'pabrik', 'bangunan', 'konstruksi', 'sipil', 'energi'],
|
'keywords' => ['mesin', 'bengkel', 'listrik', 'las', 'robot', 'motor', 'teknik', 'otomasi', 'elektronik', 'instalasi', 'panel', 'mekanik', 'industri', 'manufaktur', 'pabrik', 'bangunan', 'konstruksi', 'sipil', 'energi'],
|
||||||
'preferensi_studi' => ['Sains & Teknologi'],
|
'preferensi_studi' => ['Sains & Teknologi'],
|
||||||
'bobot_mapel' => [
|
'bobot_mapel' => [
|
||||||
|
|
@ -78,7 +78,7 @@ public function run(): void
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'nama_jurusan' => 'Kesehatan',
|
'nama_jurusan' => 'Kesehatan',
|
||||||
'deskripsi' => 'Program studi yang mempelajari ilmu kesehatan, gizi, rekam medis, dan pelayanan kesehatan masyarakat.',
|
'deskripsi' => 'Jurusan yang mempelajari ilmu kesehatan, gizi, rekam medis, dan pelayanan kesehatan masyarakat.',
|
||||||
'keywords' => ['dokter', 'perawat', 'medis', 'gizi', 'kesehatan', 'pelayanan', 'terapis', 'obat', 'rumah sakit', 'klinik', 'farmasi', 'nutrisi', 'sanitasi', 'rawat', 'sehat'],
|
'keywords' => ['dokter', 'perawat', 'medis', 'gizi', 'kesehatan', 'pelayanan', 'terapis', 'obat', 'rumah sakit', 'klinik', 'farmasi', 'nutrisi', 'sanitasi', 'rawat', 'sehat'],
|
||||||
'preferensi_studi' => ['Kesehatan & Ilmu Hayat'],
|
'preferensi_studi' => ['Kesehatan & Ilmu Hayat'],
|
||||||
'bobot_mapel' => [
|
'bobot_mapel' => [
|
||||||
|
|
@ -89,7 +89,7 @@ public function run(): void
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'nama_jurusan' => 'Bahasa, Komunikasi, dan Pariwisata',
|
'nama_jurusan' => 'Bahasa, Komunikasi, dan Pariwisata',
|
||||||
'deskripsi' => 'Program studi yang mempelajari bahasa asing, komunikasi, perhotelan, dan industri pariwisata.',
|
'deskripsi' => 'Jurusan yang mempelajari bahasa asing, komunikasi, perhotelan, dan industri pariwisata.',
|
||||||
'keywords' => ['bahasa', 'komunikasi', 'pariwisata', 'tour guide', 'hotel', 'jurnalis', 'marketing', 'inggris', 'penerjemah', 'travel', 'wisata', 'hospitality', 'public speaking', 'media', 'broadcasting'],
|
'keywords' => ['bahasa', 'komunikasi', 'pariwisata', 'tour guide', 'hotel', 'jurnalis', 'marketing', 'inggris', 'penerjemah', 'travel', 'wisata', 'hospitality', 'public speaking', 'media', 'broadcasting'],
|
||||||
'preferensi_studi' => ['Sosial & Humaniora', 'Bisnis & Manajemen'],
|
'preferensi_studi' => ['Sosial & Humaniora', 'Bisnis & Manajemen'],
|
||||||
'bobot_mapel' => [
|
'bobot_mapel' => [
|
||||||
|
|
@ -100,7 +100,7 @@ public function run(): void
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'nama_jurusan' => 'Bisnis',
|
'nama_jurusan' => 'Bisnis',
|
||||||
'deskripsi' => 'Program studi yang mempelajari akuntansi, manajemen bisnis, perbankan, dan administrasi niaga.',
|
'deskripsi' => 'Jurusan yang mempelajari akuntansi, manajemen bisnis, perbankan, dan administrasi niaga.',
|
||||||
'keywords' => ['manager', 'pimpinan', 'bisnis', 'accounting', 'marketing', 'sales', 'kantor', 'keuangan', 'bank', 'akuntansi', 'hitung', 'administrasi', 'perbankan', 'ekonomi', 'uang', 'investasi', 'pajak'],
|
'keywords' => ['manager', 'pimpinan', 'bisnis', 'accounting', 'marketing', 'sales', 'kantor', 'keuangan', 'bank', 'akuntansi', 'hitung', 'administrasi', 'perbankan', 'ekonomi', 'uang', 'investasi', 'pajak'],
|
||||||
'preferensi_studi' => ['Bisnis & Manajemen'],
|
'preferensi_studi' => ['Bisnis & Manajemen'],
|
||||||
'bobot_mapel' => [
|
'bobot_mapel' => [
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@
|
||||||
<span class="text-sm font-semibold text-gray-700 w-20">{{ $stat->kelompok_asal ?? 'Tidak Ada' }}</span>
|
<span class="text-sm font-semibold text-gray-700 w-20">{{ $stat->kelompok_asal ?? 'Tidak Ada' }}</span>
|
||||||
<div class="flex-1 h-6 bg-gray-200 rounded">
|
<div class="flex-1 h-6 bg-gray-200 rounded">
|
||||||
<div class="h-full rounded flex items-center justify-center text-white text-xs font-bold"
|
<div class="h-full rounded flex items-center justify-center text-white text-xs font-bold"
|
||||||
style="width: {{ ($stat->count / $totalSiswa) * 100 }}%; background-color: {{ $stat->kelompok_asal == 'IPA' ? '#0369A1' : '#D97706' }};">
|
style="width: {{ $totalSiswa > 0 ? ($stat->count / $totalSiswa) * 100 : 0 }}%; background-color: {{ $stat->kelompok_asal == 'IPA' ? '#0369A1' : '#D97706' }};">
|
||||||
{{ $stat->count }}
|
{{ $stat->count }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -115,10 +115,11 @@
|
||||||
@foreach($recentRecommendations as $rec)
|
@foreach($recentRecommendations as $rec)
|
||||||
@php
|
@php
|
||||||
$topJurusan = $rec->hasil_rekomendasi[0]['jurusan'] ?? '-';
|
$topJurusan = $rec->hasil_rekomendasi[0]['jurusan'] ?? '-';
|
||||||
$topSkor = round(($rec->hasil_rekomendasi[0]['skor'] ?? 0) * 100, 1);
|
$skorRaw = $rec->hasil_rekomendasi[0]['skor'] ?? 0;
|
||||||
|
$topSkor = round(($skorRaw > 1 ? $skorRaw : $skorRaw * 100), 1);
|
||||||
@endphp
|
@endphp
|
||||||
<tr class="hover:bg-gray-50">
|
<tr class="hover:bg-gray-50">
|
||||||
<td class="px-4 py-2 font-semibold text-gray-800">{{ $rec->user->name }}</td>
|
<td class="px-4 py-2 font-semibold text-gray-800">{{ $rec->user->name ?? 'Pengguna Dihapus' }}</td>
|
||||||
<td class="px-4 py-2 text-gray-700">{{ $topJurusan }}</td>
|
<td class="px-4 py-2 text-gray-700">{{ $topJurusan }}</td>
|
||||||
<td class="px-4 py-2 text-center">
|
<td class="px-4 py-2 text-center">
|
||||||
<span class="px-2 py-1 rounded bg-green-100 text-green-800 font-bold">{{ $topSkor }}%</span>
|
<span class="px-2 py-1 rounded bg-green-100 text-green-800 font-bold">{{ $topSkor }}%</span>
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,74 @@
|
||||||
.hover\:bg-maroon:hover { background-color: #7B9BA5; }
|
.hover\:bg-maroon:hover { background-color: #7B9BA5; }
|
||||||
.stat-card { transition: all 0.3s ease; }
|
.stat-card { transition: all 0.3s ease; }
|
||||||
.stat-card:hover { transform: translateY(-5px); box-shadow: 0 5px 15px rgba(91, 123, 137, 0.1); }
|
.stat-card:hover { transform: translateY(-5px); box-shadow: 0 5px 15px rgba(91, 123, 137, 0.1); }
|
||||||
.sidebar-link { transition: all 0.2s ease; }
|
|
||||||
.sidebar-link:hover { background: rgba(255,255,255,0.1); }
|
/* Sidebar */
|
||||||
.sidebar-link.active { background: #F0F4F8; color: #5B7B89 !important; }
|
.sidebar-dark {
|
||||||
|
background: linear-gradient(180deg, #1e293b 0%, #0f172a 100%);
|
||||||
|
}
|
||||||
|
.sidebar-link {
|
||||||
|
transition: all 0.25s cubic-bezier(.4,0,.2,1);
|
||||||
|
border-left: 3px solid transparent;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
.sidebar-link:hover {
|
||||||
|
background: rgba(91, 123, 137, 0.15);
|
||||||
|
color: #e2e8f0;
|
||||||
|
border-left-color: rgba(91, 123, 137, 0.5);
|
||||||
|
}
|
||||||
|
.sidebar-link.active {
|
||||||
|
background: linear-gradient(90deg, rgba(91,123,137,0.25) 0%, rgba(91,123,137,0.05) 100%);
|
||||||
|
color: #7dd3fc !important;
|
||||||
|
border-left-color: #7dd3fc;
|
||||||
|
}
|
||||||
|
.sidebar-link .sidebar-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(255,255,255,0.06);
|
||||||
|
font-size: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
}
|
||||||
|
.sidebar-link:hover .sidebar-icon {
|
||||||
|
background: rgba(91, 123, 137, 0.3);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
.sidebar-link.active .sidebar-icon {
|
||||||
|
background: rgba(125, 211, 252, 0.15);
|
||||||
|
}
|
||||||
|
.sidebar-section-label {
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #475569;
|
||||||
|
padding: 0 1rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.sidebar-brand {
|
||||||
|
padding: 1.25rem 1rem;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.06);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.sidebar-brand-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background: linear-gradient(135deg, #5B7B89 0%, #7B9BA5 100%);
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 20px;
|
||||||
|
box-shadow: 0 4px 12px rgba(91, 123, 137, 0.3);
|
||||||
|
}
|
||||||
|
.sidebar-footer {
|
||||||
|
border-top: 1px solid rgba(255,255,255,0.06);
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.sidebar-desktop { display: none; }
|
.sidebar-desktop { display: none; }
|
||||||
.sidebar-mobile.open { display: block; }
|
.sidebar-mobile.open { display: block; }
|
||||||
|
|
@ -67,61 +132,92 @@
|
||||||
|
|
||||||
<div class="flex min-h-screen">
|
<div class="flex min-h-screen">
|
||||||
<!-- Sidebar Desktop -->
|
<!-- Sidebar Desktop -->
|
||||||
<aside class="hidden md:block w-64 gradient-maroon text-white shadow-lg flex-shrink-0">
|
<aside class="hidden md:block w-64 sidebar-dark shadow-2xl flex-shrink-0">
|
||||||
<nav class="p-4 space-y-1 sticky top-20">
|
<div class="sticky top-20">
|
||||||
<p class="text-xs text-gray-300 font-bold uppercase tracking-wider mb-4 px-4">Menu Utama</p>
|
<!-- Brand -->
|
||||||
|
<div class="sidebar-brand">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="sidebar-brand-icon">🎓</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-white font-bold text-sm leading-tight">SPK Jurusan</p>
|
||||||
|
<p class="text-xs text-slate-400">Admin Panel</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<a href="{{ route('admin.dashboard') }}" class="sidebar-link block px-4 py-3 rounded-lg font-semibold text-sm {{ request()->routeIs('admin.dashboard') ? 'active' : '' }}">
|
<nav class="px-3 py-2 space-y-1">
|
||||||
📊 Dashboard
|
<p class="sidebar-section-label mt-2">Menu Utama</p>
|
||||||
</a>
|
|
||||||
<a href="{{ route('admin.students') }}" class="sidebar-link block px-4 py-3 rounded-lg font-semibold text-sm {{ request()->routeIs('admin.students*') ? 'active' : '' }}">
|
|
||||||
👥 Manajemen Data Siswa
|
|
||||||
</a>
|
|
||||||
<a href="{{ route('admin.jurusan') }}" class="sidebar-link block px-4 py-3 rounded-lg font-semibold text-sm {{ request()->routeIs('admin.jurusan*') ? 'active' : '' }}">
|
|
||||||
🎓 Manajemen Jurusan
|
|
||||||
</a>
|
|
||||||
<a href="{{ route('admin.guru-bk') }}" class="sidebar-link block px-4 py-3 rounded-lg font-semibold text-sm {{ request()->routeIs('admin.guru-bk*') ? 'active' : '' }}">
|
|
||||||
👨🏫 Manajemen Akun Guru BK
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<p class="text-xs text-gray-300 font-bold uppercase tracking-wider mt-6 mb-3 px-4">Riwayat</p>
|
<a href="{{ route('admin.dashboard') }}" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium {{ request()->routeIs('admin.dashboard') ? 'active' : '' }}">
|
||||||
|
<span class="sidebar-icon">📊</span> Dashboard
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('admin.students') }}" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium {{ request()->routeIs('admin.students*') ? 'active' : '' }}">
|
||||||
|
<span class="sidebar-icon">👥</span> Data Siswa
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('admin.jurusan') }}" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium {{ request()->routeIs('admin.jurusan*') ? 'active' : '' }}">
|
||||||
|
<span class="sidebar-icon">🏛️</span> Jurusan
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('admin.guru-bk') }}" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium {{ request()->routeIs('admin.guru-bk*') ? 'active' : '' }}">
|
||||||
|
<span class="sidebar-icon">👨🏫</span> Akun Guru BK
|
||||||
|
</a>
|
||||||
|
|
||||||
<a href="{{ route('admin.riwayat-rekomendasi') }}" class="sidebar-link block px-4 py-3 rounded-lg font-semibold text-sm {{ request()->routeIs('admin.riwayat-rekomendasi*') ? 'active' : '' }}">
|
<p class="sidebar-section-label mt-5">Riwayat</p>
|
||||||
🎯 Riwayat Rekomendasi
|
|
||||||
</a>
|
<a href="{{ route('admin.riwayat-rekomendasi') }}" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium {{ request()->routeIs('admin.riwayat-rekomendasi*') ? 'active' : '' }}">
|
||||||
<a href="{{ route('admin.riwayat-chatbot') }}" class="sidebar-link block px-4 py-3 rounded-lg font-semibold text-sm {{ request()->routeIs('admin.riwayat-chatbot*') ? 'active' : '' }}">
|
<span class="sidebar-icon">🎯</span> Rekomendasi
|
||||||
💬 Riwayat Konsultasi Chatbot
|
</a>
|
||||||
</a>
|
<a href="{{ route('admin.riwayat-chatbot') }}" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium {{ request()->routeIs('admin.riwayat-chatbot*') ? 'active' : '' }}">
|
||||||
</nav>
|
<span class="sidebar-icon">💬</span> Konsultasi Chatbot
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div class="sidebar-footer mt-4">
|
||||||
|
<div class="flex items-center gap-3 px-3 py-2">
|
||||||
|
<div class="w-8 h-8 rounded-full bg-gradient-to-br from-blue-400 to-cyan-300 flex items-center justify-center text-white text-xs font-bold">
|
||||||
|
{{ strtoupper(substr(Auth::user()->name, 0, 1)) }}
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<p class="text-xs font-medium text-slate-300 truncate">{{ Auth::user()->name }}</p>
|
||||||
|
<p class="text-xs text-slate-500">{{ Auth::user()->role === 'admin' ? 'Administrator' : (Auth::user()->role === 'bk' ? 'Guru BK' : ucfirst(Auth::user()->role)) }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<!-- Mobile Sidebar Overlay -->
|
<!-- Mobile Sidebar Overlay -->
|
||||||
<div id="mobileSidebar" class="fixed inset-0 z-40 hidden">
|
<div id="mobileSidebar" class="fixed inset-0 z-40 hidden">
|
||||||
<div class="absolute inset-0 bg-black bg-opacity-50" id="mobileOverlay"></div>
|
<div class="absolute inset-0 bg-black bg-opacity-60 backdrop-blur-sm" id="mobileOverlay"></div>
|
||||||
<aside class="relative w-64 h-full gradient-maroon text-white shadow-lg overflow-y-auto">
|
<aside class="relative w-72 h-full sidebar-dark shadow-2xl overflow-y-auto">
|
||||||
<div class="p-4 border-b border-white border-opacity-20 flex justify-between items-center">
|
<div class="p-4 flex justify-between items-center" style="border-bottom: 1px solid rgba(255,255,255,0.06);">
|
||||||
<span class="font-bold">Menu Admin</span>
|
<div class="flex items-center gap-3">
|
||||||
<button id="closeMobileMenu" class="text-white">✕</button>
|
<div class="sidebar-brand-icon" style="width:36px;height:36px;font-size:18px;">🎓</div>
|
||||||
|
<span class="font-bold text-white text-sm">SPK Jurusan</span>
|
||||||
|
</div>
|
||||||
|
<button id="closeMobileMenu" class="text-slate-400 hover:text-white transition text-xl">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<nav class="p-4 space-y-1">
|
<nav class="px-3 py-3 space-y-1">
|
||||||
<a href="{{ route('admin.dashboard') }}" class="sidebar-link block px-4 py-3 rounded-lg font-semibold text-sm {{ request()->routeIs('admin.dashboard') ? 'active' : '' }}">
|
<p class="sidebar-section-label mt-1">Menu Utama</p>
|
||||||
📊 Dashboard
|
<a href="{{ route('admin.dashboard') }}" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium {{ request()->routeIs('admin.dashboard') ? 'active' : '' }}">
|
||||||
|
<span class="sidebar-icon">📊</span> Dashboard
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ route('admin.students') }}" class="sidebar-link block px-4 py-3 rounded-lg font-semibold text-sm {{ request()->routeIs('admin.students*') ? 'active' : '' }}">
|
<a href="{{ route('admin.students') }}" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium {{ request()->routeIs('admin.students*') ? 'active' : '' }}">
|
||||||
👥 Manajemen Data Siswa
|
<span class="sidebar-icon">👥</span> Data Siswa
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ route('admin.jurusan') }}" class="sidebar-link block px-4 py-3 rounded-lg font-semibold text-sm {{ request()->routeIs('admin.jurusan*') ? 'active' : '' }}">
|
<a href="{{ route('admin.jurusan') }}" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium {{ request()->routeIs('admin.jurusan*') ? 'active' : '' }}">
|
||||||
🎓 Manajemen Jurusan
|
<span class="sidebar-icon">🏛️</span> Jurusan
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ route('admin.guru-bk') }}" class="sidebar-link block px-4 py-3 rounded-lg font-semibold text-sm {{ request()->routeIs('admin.guru-bk*') ? 'active' : '' }}">
|
<a href="{{ route('admin.guru-bk') }}" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium {{ request()->routeIs('admin.guru-bk*') ? 'active' : '' }}">
|
||||||
👨🏫 Manajemen Akun Guru BK
|
<span class="sidebar-icon">👨🏫</span> Akun Guru BK
|
||||||
</a>
|
</a>
|
||||||
<hr class="border-white border-opacity-20 my-3">
|
|
||||||
<a href="{{ route('admin.riwayat-rekomendasi') }}" class="sidebar-link block px-4 py-3 rounded-lg font-semibold text-sm {{ request()->routeIs('admin.riwayat-rekomendasi*') ? 'active' : '' }}">
|
<p class="sidebar-section-label mt-5">Riwayat</p>
|
||||||
🎯 Riwayat Rekomendasi
|
<a href="{{ route('admin.riwayat-rekomendasi') }}" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium {{ request()->routeIs('admin.riwayat-rekomendasi*') ? 'active' : '' }}">
|
||||||
|
<span class="sidebar-icon">🎯</span> Rekomendasi
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ route('admin.riwayat-chatbot') }}" class="sidebar-link block px-4 py-3 rounded-lg font-semibold text-sm {{ request()->routeIs('admin.riwayat-chatbot*') ? 'active' : '' }}">
|
<a href="{{ route('admin.riwayat-chatbot') }}" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium {{ request()->routeIs('admin.riwayat-chatbot*') ? 'active' : '' }}">
|
||||||
💬 Riwayat Konsultasi Chatbot
|
<span class="sidebar-icon">💬</span> Konsultasi Chatbot
|
||||||
</a>
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,8 @@
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="w-5 h-5 flex items-center justify-center rounded-full text-xs font-bold {{ $i === 0 ? 'bg-yellow-400 text-yellow-900' : 'bg-gray-200 text-gray-600' }}">{{ $i + 1 }}</span>
|
<span class="w-5 h-5 flex items-center justify-center rounded-full text-xs font-bold {{ $i === 0 ? 'bg-yellow-400 text-yellow-900' : 'bg-gray-200 text-gray-600' }}">{{ $i + 1 }}</span>
|
||||||
<span class="text-xs text-gray-700">{{ $hasil['jurusan'] ?? 'N/A' }}</span>
|
<span class="text-xs text-gray-700">{{ $hasil['jurusan'] ?? 'N/A' }}</span>
|
||||||
<span class="text-xs font-semibold {{ $i === 0 ? 'text-blue-600' : 'text-gray-400' }}">{{ round(($hasil['skor'] ?? 0) * 100, 1) }}%</span>
|
@php $skorVal = $hasil['skor'] ?? 0; @endphp
|
||||||
|
<span class="text-xs font-semibold {{ $i === 0 ? 'text-blue-600' : 'text-gray-400' }}">{{ round(($skorVal > 1 ? $skorVal : $skorVal * 100), 1) }}%</span>
|
||||||
</div>
|
</div>
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -75,8 +75,9 @@
|
||||||
@foreach(array_slice($rec->hasil_rekomendasi, 0, 3) as $idx => $hasil)
|
@foreach(array_slice($rec->hasil_rekomendasi, 0, 3) as $idx => $hasil)
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-sm text-gray-700">{{ $idx + 1 }}. {{ $hasil['jurusan'] ?? 'N/A' }}</span>
|
<span class="text-sm text-gray-700">{{ $idx + 1 }}. {{ $hasil['jurusan'] ?? 'N/A' }}</span>
|
||||||
|
@php $skorVal = $hasil['skor'] ?? 0; @endphp
|
||||||
<span class="px-2 py-1 rounded text-xs font-bold" style="background-color: {{ $idx === 0 ? '#DBEAFE' : '#F3F4F6' }}; color: {{ $idx === 0 ? '#1e40af' : '#6B7280' }};">
|
<span class="px-2 py-1 rounded text-xs font-bold" style="background-color: {{ $idx === 0 ? '#DBEAFE' : '#F3F4F6' }}; color: {{ $idx === 0 ? '#1e40af' : '#6B7280' }};">
|
||||||
{{ round(($hasil['skor'] ?? 0) * 100, 1) }}%
|
{{ round(($skorVal > 1 ? $skorVal : $skorVal * 100), 1) }}%
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@endforeach
|
@endforeach
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
@extends('bk.layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'Chat History - ' . ($user->name ?? 'Unknown'))
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-6">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-bold text-bk">💬 Chat History</h2>
|
||||||
|
<p class="text-sm text-gray-500 mt-1">{{ $user->name ?? 'Unknown' }}</p>
|
||||||
|
</div>
|
||||||
|
<a href="{{ route('bk.student.detail', $user->id) }}" class="bg-gray-400 text-white font-bold py-2 px-4 rounded-lg hover:bg-gray-500 transition text-sm">
|
||||||
|
← Profil Siswa
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Stats -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
|
||||||
|
<div class="bg-white rounded-lg shadow p-4 border-t-4 border-teal-500">
|
||||||
|
<p class="text-gray-600 text-sm font-semibold">Total Chat</p>
|
||||||
|
<p class="text-2xl font-bold text-bk mt-1">{{ $chatHistories->count() }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white rounded-lg shadow p-4 border-t-4 border-blue-400">
|
||||||
|
<p class="text-gray-600 text-sm font-semibold">Pertanyaan</p>
|
||||||
|
<p class="text-2xl font-bold text-blue-600 mt-1">{{ $chatHistories->count() }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white rounded-lg shadow p-4 border-t-4 border-green-400">
|
||||||
|
<p class="text-gray-600 text-sm font-semibold">Jawaban</p>
|
||||||
|
<p class="text-2xl font-bold text-green-600 mt-1">{{ $chatHistories->count() }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Chat Messages -->
|
||||||
|
<div class="bg-white rounded-lg shadow p-6 space-y-4 max-w-4xl">
|
||||||
|
@forelse($chatHistories->sortBy('created_at') as $chat)
|
||||||
|
<div class="mb-6">
|
||||||
|
<p class="text-xs text-gray-500 text-center mb-3">
|
||||||
|
{{ $chat->created_at->format('d/m/Y H:i') }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Question -->
|
||||||
|
<div class="flex justify-start mb-2">
|
||||||
|
<div class="bg-blue-100 text-blue-900 rounded-lg p-3 mb-2 max-w-md break-words">
|
||||||
|
<p class="text-sm font-semibold mb-1">Siswa 📤</p>
|
||||||
|
<p class="text-sm leading-relaxed">{{ $chat->prompt }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Answer -->
|
||||||
|
<div class="flex justify-end mb-2">
|
||||||
|
<div class="bg-green-100 text-green-900 rounded-lg p-3 mb-2 max-w-2xl break-words">
|
||||||
|
<p class="text-sm font-semibold mb-1 text-right">Konselor BK 📥</p>
|
||||||
|
<p class="text-sm leading-relaxed whitespace-pre-wrap">{{ $chat->response }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if(!$loop->last)
|
||||||
|
<hr class="my-4 border-gray-200">
|
||||||
|
@endif
|
||||||
|
@empty
|
||||||
|
<div class="text-center py-12">
|
||||||
|
<p class="text-gray-500 text-sm">📭 Belum ada chat history untuk siswa ini</p>
|
||||||
|
</div>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Info Box -->
|
||||||
|
<div class="mt-8 p-4 bg-teal-50 border-l-4 border-teal-400 rounded max-w-4xl">
|
||||||
|
<p class="text-sm text-teal-800">
|
||||||
|
<strong>ℹ️ Info:</strong> Chat history menunjukkan interaksi siswa dengan Konselor BK AI
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
@ -0,0 +1,141 @@
|
||||||
|
@extends('bk.layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'Dashboard')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="mb-6">
|
||||||
|
<h2 class="text-2xl font-bold text-bk">📊 Dashboard Guru BK</h2>
|
||||||
|
<p class="text-sm text-gray-500 mt-1">Selamat datang, {{ Auth::user()->name }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Statistics Cards -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
||||||
|
<div class="stat-card bg-white rounded-lg shadow p-6 border-t-4 border-teal-500">
|
||||||
|
<p class="text-gray-600 text-sm font-semibold">👥 Total Siswa</p>
|
||||||
|
<p class="text-3xl font-bold text-bk mt-2">{{ $totalSiswa }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card bg-white rounded-lg shadow p-6 border-t-4 border-yellow-400">
|
||||||
|
<p class="text-gray-600 text-sm font-semibold">🎯 Total Rekomendasi</p>
|
||||||
|
<p class="text-3xl font-bold mt-2" style="color: #EA580C;">{{ $totalRekomendasi }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card bg-white rounded-lg shadow p-6 border-t-4 border-blue-400">
|
||||||
|
<p class="text-gray-600 text-sm font-semibold">💬 Chat History</p>
|
||||||
|
<p class="text-3xl font-bold text-blue-600 mt-2">{{ $totalChatHistory }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card bg-white rounded-lg shadow p-6 border-t-4 border-green-400">
|
||||||
|
<p class="text-gray-600 text-sm font-semibold">🎓 Jurusan Tersedia</p>
|
||||||
|
<p class="text-3xl font-bold text-green-600 mt-2">{{ $totalJurusan }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Kelompok Distribution & Top Majors -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||||
|
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-teal-500">
|
||||||
|
<h3 class="text-lg font-bold text-bk mb-4">📊 Siswa per Kelompok</h3>
|
||||||
|
<div class="space-y-3">
|
||||||
|
@foreach($kelompokStats as $stat)
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="text-sm font-semibold text-gray-700 w-20">{{ $stat->kelompok_asal ?? 'Tidak Ada' }}</span>
|
||||||
|
<div class="flex-1 h-6 bg-gray-200 rounded">
|
||||||
|
<div class="h-full rounded flex items-center justify-center text-white text-xs font-bold"
|
||||||
|
style="width: {{ $totalSiswa > 0 ? ($stat->count / $totalSiswa) * 100 : 0 }}%; background-color: {{ $stat->kelompok_asal == 'IPA' ? '#0369A1' : '#D97706' }};">
|
||||||
|
{{ $stat->count }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-green-400">
|
||||||
|
<h3 class="text-lg font-bold text-bk mb-4">🎯 Jurusan Terpopuler</h3>
|
||||||
|
@if($topMajors->isNotEmpty())
|
||||||
|
<div class="space-y-3">
|
||||||
|
@foreach($topMajors as $major)
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="text-sm font-semibold text-gray-700 flex-1 truncate">{{ $major->major_name }}</span>
|
||||||
|
<span class="px-3 py-1 rounded bg-green-100 text-green-800 font-bold text-sm">{{ $major->count }}</span>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<p class="text-gray-500 text-sm">Belum ada data rekomendasi</p>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Recent Students -->
|
||||||
|
<div class="bg-white rounded-lg shadow p-6 mb-6 border-l-4 border-yellow-400">
|
||||||
|
<h3 class="text-lg font-bold text-bk mb-4">👥 Siswa Terbaru</h3>
|
||||||
|
@if($recentStudents->isNotEmpty())
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-sm">
|
||||||
|
<thead class="border-b-2 border-teal-500">
|
||||||
|
<tr>
|
||||||
|
<th class="text-left px-4 py-2 font-bold text-bk">Nama</th>
|
||||||
|
<th class="text-center px-4 py-2 font-bold text-bk">NIS</th>
|
||||||
|
<th class="text-center px-4 py-2 font-bold text-bk">Kelompok</th>
|
||||||
|
<th class="text-center px-4 py-2 font-bold text-bk">Aksi</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y">
|
||||||
|
@foreach($recentStudents as $student)
|
||||||
|
<tr class="hover:bg-gray-50">
|
||||||
|
<td class="px-4 py-2 font-semibold text-gray-800">{{ $student->name }}</td>
|
||||||
|
<td class="px-4 py-2 text-center text-gray-600">{{ $student->nis ?? '-' }}</td>
|
||||||
|
<td class="px-4 py-2 text-center">
|
||||||
|
<span class="px-2 py-1 rounded text-xs font-bold" style="{{ $student->kelompok_asal == 'IPA' ? 'background-color: #E0F2FE; color: #0369A1;' : 'background-color: #FEF3C7; color: #92400E;' }}">
|
||||||
|
{{ $student->kelompok_asal ?? '-' }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-2 text-center">
|
||||||
|
<a href="{{ route('bk.student.detail', $student->id) }}" class="text-teal-600 hover:text-teal-800 font-semibold text-xs">👁 Lihat</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<p class="text-gray-500 text-sm">Belum ada siswa terdaftar</p>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Recent Recommendations -->
|
||||||
|
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-blue-400">
|
||||||
|
<h3 class="text-lg font-bold text-bk mb-4">🎯 Rekomendasi Terbaru</h3>
|
||||||
|
@if($recentRecommendations->isNotEmpty())
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-sm">
|
||||||
|
<thead class="border-b-2 border-teal-500">
|
||||||
|
<tr>
|
||||||
|
<th class="text-left px-4 py-2 font-bold text-bk">Siswa</th>
|
||||||
|
<th class="text-left px-4 py-2 font-bold text-bk">Top Rekomendasi</th>
|
||||||
|
<th class="text-center px-4 py-2 font-bold text-bk">Skor</th>
|
||||||
|
<th class="text-center px-4 py-2 font-bold text-bk">Tanggal</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y">
|
||||||
|
@foreach($recentRecommendations as $rec)
|
||||||
|
@php
|
||||||
|
$topJurusan = $rec->hasil_rekomendasi[0]['jurusan'] ?? '-';
|
||||||
|
$skorRaw = $rec->hasil_rekomendasi[0]['skor'] ?? 0;
|
||||||
|
$topSkor = round(($skorRaw > 1 ? $skorRaw : $skorRaw * 100), 1);
|
||||||
|
@endphp
|
||||||
|
<tr class="hover:bg-gray-50">
|
||||||
|
<td class="px-4 py-2 font-semibold text-gray-800">{{ $rec->user->name ?? 'Pengguna Dihapus' }}</td>
|
||||||
|
<td class="px-4 py-2 text-gray-700">{{ $topJurusan }}</td>
|
||||||
|
<td class="px-4 py-2 text-center">
|
||||||
|
<span class="px-2 py-1 rounded bg-green-100 text-green-800 font-bold">{{ $topSkor }}%</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-2 text-center text-gray-600">{{ $rec->created_at->format('d M Y') }}</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<p class="text-gray-500 text-sm">Belum ada rekomendasi</p>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
@extends('bk.layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'Tambah Jurusan')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-bold text-bk">➕ Tambah Jurusan Baru</h2>
|
||||||
|
<p class="text-sm text-gray-500 mt-1">Tambahkan jurusan baru ke dalam sistem rekomendasi</p>
|
||||||
|
</div>
|
||||||
|
<a href="{{ route('bk.jurusan') }}" class="bg-gray-400 text-white font-bold py-2 px-4 rounded-lg hover:bg-gray-500 transition text-sm">
|
||||||
|
← Kembali
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($errors->any())
|
||||||
|
<div class="bg-red-50 border-l-4 border-red-400 p-4 rounded-lg mb-6">
|
||||||
|
<ul class="text-red-800 text-sm list-disc list-inside">
|
||||||
|
@foreach($errors->all() as $error)
|
||||||
|
<li>{{ $error }}</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<form action="{{ route('bk.jurusan.store') }}" method="POST" class="space-y-6">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<div class="bg-white rounded-lg shadow p-6">
|
||||||
|
<h3 class="text-lg font-bold text-bk mb-4">📋 Informasi Jurusan</h3>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-semibold text-gray-700 mb-2">Nama Jurusan <span class="text-red-500">*</span></label>
|
||||||
|
<input type="text" name="nama_jurusan" value="{{ old('nama_jurusan') }}" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500" placeholder="Contoh: Teknologi Informasi" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-semibold text-gray-700 mb-2">Deskripsi</label>
|
||||||
|
<textarea name="deskripsi" rows="3" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500" placeholder="Jelaskan singkat tentang jurusan ini">{{ old('deskripsi') }}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-semibold text-gray-700 mb-2">Keywords (Kata Kunci Minat & Cita-cita)</label>
|
||||||
|
<textarea name="keywords" rows="3" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500" placeholder="Pisahkan dengan koma, contoh: programmer, developer, coding, software, web">{{ old('keywords') }}</textarea>
|
||||||
|
<p class="text-xs text-gray-500 mt-1">Keywords digunakan untuk mencocokkan minat dan cita-cita siswa dengan jurusan ini. Pisahkan dengan koma.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-semibold text-gray-700 mb-2">Preferensi Studi</label>
|
||||||
|
<textarea name="preferensi_studi" rows="2" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500" placeholder="Pisahkan dengan koma, contoh: Sains & Teknologi, Pertanian & Lingkungan">{{ old('preferensi_studi') }}</textarea>
|
||||||
|
<p class="text-xs text-gray-500 mt-1">Rumpun bidang studi yang cocok untuk jurusan ini. Pilihan: Sains & Teknologi, Pertanian & Lingkungan, Kesehatan & Ilmu Hayat, Bisnis & Manajemen, Sosial & Humaniora</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-semibold text-gray-700 mb-2">Prospek Kerja</label>
|
||||||
|
<textarea name="prospek_kerja" rows="2" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500" placeholder="Contoh: Software Developer, Web Developer, Data Analyst">{{ old('prospek_kerja') }}</textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bobot Mata Pelajaran -->
|
||||||
|
<div class="bg-white rounded-lg shadow p-6">
|
||||||
|
<h3 class="text-lg font-bold text-bk mb-4">⚖️ Bobot Mata Pelajaran</h3>
|
||||||
|
<p class="text-xs text-gray-500 mb-4">Tentukan bobot setiap mata pelajaran untuk jurusan ini (0.00 - 1.00). Mata pelajaran yang lebih relevan diberi bobot lebih tinggi. Jumlah total tidak harus 1.0.</p>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<h4 class="text-sm font-bold text-gray-700 mb-3 border-b pb-2">📐 IPA</h4>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Matematika</label>
|
||||||
|
<input type="number" name="bobot_mtk" value="{{ old('bobot_mtk', '0.25') }}" step="0.05" min="0" max="1" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500 text-sm">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Fisika</label>
|
||||||
|
<input type="number" name="bobot_fisika" value="{{ old('bobot_fisika', '0.25') }}" step="0.05" min="0" max="1" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500 text-sm">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Kimia</label>
|
||||||
|
<input type="number" name="bobot_kimia" value="{{ old('bobot_kimia', '0.25') }}" step="0.05" min="0" max="1" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500 text-sm">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Biologi</label>
|
||||||
|
<input type="number" name="bobot_biologi" value="{{ old('bobot_biologi', '0.25') }}" step="0.05" min="0" max="1" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500 text-sm">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="text-sm font-bold text-gray-700 mb-3 border-b pb-2">📊 IPS</h4>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Ekonomi</label>
|
||||||
|
<input type="number" name="bobot_ekonomi" value="{{ old('bobot_ekonomi', '0.25') }}" step="0.05" min="0" max="1" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500 text-sm">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Geografi</label>
|
||||||
|
<input type="number" name="bobot_geografi" value="{{ old('bobot_geografi', '0.25') }}" step="0.05" min="0" max="1" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500 text-sm">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Sosiologi</label>
|
||||||
|
<input type="number" name="bobot_sosiologi" value="{{ old('bobot_sosiologi', '0.25') }}" step="0.05" min="0" max="1" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500 text-sm">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Sejarah</label>
|
||||||
|
<input type="number" name="bobot_sejarah" value="{{ old('bobot_sejarah', '0.25') }}" step="0.05" min="0" max="1" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500 text-sm">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<button type="submit" class="flex-1 gradient-bk text-white font-bold py-3 px-4 rounded-lg hover:opacity-90 transition">
|
||||||
|
💾 Simpan Jurusan
|
||||||
|
</button>
|
||||||
|
<a href="{{ route('bk.jurusan') }}" class="flex-1 bg-gray-400 text-white font-bold py-3 px-4 rounded-lg hover:bg-gray-500 transition text-center">
|
||||||
|
Batal
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
@endsection
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
@extends('bk.layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'Edit Jurusan')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-bold text-bk">✏️ Edit Jurusan: {{ $jurusan->nama_jurusan }}</h2>
|
||||||
|
<p class="text-sm text-gray-500 mt-1">Ubah informasi jurusan</p>
|
||||||
|
</div>
|
||||||
|
<a href="{{ route('bk.jurusan') }}" class="bg-gray-400 text-white font-bold py-2 px-4 rounded-lg hover:bg-gray-500 transition text-sm">
|
||||||
|
← Kembali
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($errors->any())
|
||||||
|
<div class="bg-red-50 border-l-4 border-red-400 p-4 rounded-lg mb-6">
|
||||||
|
<ul class="text-red-800 text-sm list-disc list-inside">
|
||||||
|
@foreach($errors->all() as $error)
|
||||||
|
<li>{{ $error }}</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<form action="{{ route('bk.jurusan.update', $jurusan->id) }}" method="POST" class="space-y-6">
|
||||||
|
@csrf
|
||||||
|
@method('PUT')
|
||||||
|
|
||||||
|
<div class="bg-white rounded-lg shadow p-6">
|
||||||
|
<h3 class="text-lg font-bold text-bk mb-4">📋 Informasi Jurusan</h3>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-semibold text-gray-700 mb-2">Nama Jurusan <span class="text-red-500">*</span></label>
|
||||||
|
<input type="text" name="nama_jurusan" value="{{ old('nama_jurusan', $jurusan->nama_jurusan) }}" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-semibold text-gray-700 mb-2">Deskripsi</label>
|
||||||
|
<textarea name="deskripsi" rows="3" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500" placeholder="Jelaskan singkat tentang jurusan ini">{{ old('deskripsi', $jurusan->deskripsi) }}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-semibold text-gray-700 mb-2">Keywords (Kata Kunci Minat & Cita-cita)</label>
|
||||||
|
<textarea name="keywords" rows="3" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500" placeholder="Pisahkan dengan koma">{{ old('keywords', implode(', ', $jurusan->keywords ?? [])) }}</textarea>
|
||||||
|
<p class="text-xs text-gray-500 mt-1">Keywords digunakan untuk mencocokkan minat dan cita-cita siswa dengan jurusan ini. Pisahkan dengan koma.</p>
|
||||||
|
@if(!empty($jurusan->keywords))
|
||||||
|
<div class="flex flex-wrap gap-1 mt-2">
|
||||||
|
@foreach($jurusan->keywords as $kw)
|
||||||
|
<span class="inline-block px-2 py-0.5 rounded bg-blue-100 text-blue-700 text-xs">{{ $kw }}</span>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-semibold text-gray-700 mb-2">Preferensi Studi</label>
|
||||||
|
<textarea name="preferensi_studi" rows="2" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500" placeholder="Pisahkan dengan koma">{{ old('preferensi_studi', implode(', ', $jurusan->preferensi_studi ?? [])) }}</textarea>
|
||||||
|
<p class="text-xs text-gray-500 mt-1">Rumpun bidang studi yang cocok: Sains & Teknologi, Pertanian & Lingkungan, Kesehatan & Ilmu Hayat, Bisnis & Manajemen, Sosial & Humaniora</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-semibold text-gray-700 mb-2">Prospek Kerja</label>
|
||||||
|
<textarea name="prospek_kerja" rows="2" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500" placeholder="Contoh: Software Developer, Web Developer, Data Analyst">{{ old('prospek_kerja', $jurusan->prospek_kerja) }}</textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bobot Mata Pelajaran -->
|
||||||
|
@php
|
||||||
|
$bobot = $jurusan->bobot_mapel ?? [];
|
||||||
|
@endphp
|
||||||
|
<div class="bg-white rounded-lg shadow p-6">
|
||||||
|
<h3 class="text-lg font-bold text-bk mb-4">⚖️ Bobot Mata Pelajaran</h3>
|
||||||
|
<p class="text-xs text-gray-500 mb-4">Tentukan bobot setiap mata pelajaran untuk jurusan ini (0.00 - 1.00). Mata pelajaran yang lebih relevan diberi bobot lebih tinggi. Jumlah total tidak harus 1.0.</p>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<h4 class="text-sm font-bold text-gray-700 mb-3 border-b pb-2">📐 IPA</h4>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Matematika</label>
|
||||||
|
<input type="number" name="bobot_mtk" value="{{ old('bobot_mtk', $bobot['mtk'] ?? '0.25') }}" step="0.05" min="0" max="1" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500 text-sm">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Fisika</label>
|
||||||
|
<input type="number" name="bobot_fisika" value="{{ old('bobot_fisika', $bobot['fisika'] ?? '0.25') }}" step="0.05" min="0" max="1" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500 text-sm">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Kimia</label>
|
||||||
|
<input type="number" name="bobot_kimia" value="{{ old('bobot_kimia', $bobot['kimia'] ?? '0.25') }}" step="0.05" min="0" max="1" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500 text-sm">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Biologi</label>
|
||||||
|
<input type="number" name="bobot_biologi" value="{{ old('bobot_biologi', $bobot['biologi'] ?? '0.25') }}" step="0.05" min="0" max="1" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500 text-sm">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="text-sm font-bold text-gray-700 mb-3 border-b pb-2">📊 IPS</h4>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Ekonomi</label>
|
||||||
|
<input type="number" name="bobot_ekonomi" value="{{ old('bobot_ekonomi', $bobot['ekonomi'] ?? '0.25') }}" step="0.05" min="0" max="1" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500 text-sm">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Geografi</label>
|
||||||
|
<input type="number" name="bobot_geografi" value="{{ old('bobot_geografi', $bobot['geografi'] ?? '0.25') }}" step="0.05" min="0" max="1" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500 text-sm">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Sosiologi</label>
|
||||||
|
<input type="number" name="bobot_sosiologi" value="{{ old('bobot_sosiologi', $bobot['sosiologi'] ?? '0.25') }}" step="0.05" min="0" max="1" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500 text-sm">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Sejarah</label>
|
||||||
|
<input type="number" name="bobot_sejarah" value="{{ old('bobot_sejarah', $bobot['sejarah'] ?? '0.25') }}" step="0.05" min="0" max="1" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-teal-500 text-sm">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<button type="submit" class="flex-1 gradient-bk text-white font-bold py-3 px-4 rounded-lg hover:opacity-90 transition">
|
||||||
|
💾 Simpan Perubahan
|
||||||
|
</button>
|
||||||
|
<a href="{{ route('bk.jurusan') }}" class="flex-1 bg-gray-400 text-white font-bold py-3 px-4 rounded-lg hover:bg-gray-500 transition text-center">
|
||||||
|
Batal
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
@endsection
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
@extends('bk.layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'Manajemen Jurusan')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<!-- Page Header -->
|
||||||
|
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-6 gap-3">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-bold text-bk">🎓 Manajemen Jurusan</h2>
|
||||||
|
<p class="text-sm text-gray-500 mt-1">Kelola data jurusan Polije (tambah, edit, hapus)</p>
|
||||||
|
</div>
|
||||||
|
<a href="{{ route('bk.jurusan.create') }}" class="gradient-bk text-white font-bold py-2 px-4 rounded-lg hover:opacity-90 transition text-sm">
|
||||||
|
+ Tambah Jurusan
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if(session('success'))
|
||||||
|
<div class="bg-green-50 border-l-4 border-green-400 p-4 rounded-lg mb-6">
|
||||||
|
<p class="text-green-800 text-sm font-semibold">✅ {{ session('success') }}</p>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if(session('error'))
|
||||||
|
<div class="bg-red-50 border-l-4 border-red-400 p-4 rounded-lg mb-6">
|
||||||
|
<p class="text-red-800 text-sm font-semibold">❌ {{ session('error') }}</p>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<!-- Jurusan Table -->
|
||||||
|
<div class="bg-white rounded-lg shadow overflow-hidden">
|
||||||
|
<table class="w-full text-sm">
|
||||||
|
<thead class="gradient-bk text-white">
|
||||||
|
<tr>
|
||||||
|
<th class="px-4 py-3 text-left">No</th>
|
||||||
|
<th class="px-4 py-3 text-left">Nama Jurusan</th>
|
||||||
|
<th class="px-4 py-3 text-left hidden md:table-cell">Keywords</th>
|
||||||
|
<th class="px-4 py-3 text-left hidden lg:table-cell">Preferensi Studi</th>
|
||||||
|
<th class="px-4 py-3 text-center">Aksi</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y">
|
||||||
|
@forelse($jurusanList as $index => $jurusan)
|
||||||
|
<tr class="hover:bg-gray-50 transition">
|
||||||
|
<td class="px-4 py-3 text-gray-600">{{ $index + 1 }}</td>
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
<p class="font-semibold text-bk">{{ $jurusan->nama_jurusan }}</p>
|
||||||
|
@if($jurusan->deskripsi)
|
||||||
|
<p class="text-xs text-gray-500 mt-1 line-clamp-2">{{ Str::limit($jurusan->deskripsi, 80) }}</p>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 hidden md:table-cell">
|
||||||
|
<div class="flex flex-wrap gap-1">
|
||||||
|
@foreach(array_slice($jurusan->keywords ?? [], 0, 5) as $kw)
|
||||||
|
<span class="inline-block px-2 py-0.5 rounded bg-blue-100 text-blue-700 text-xs">{{ $kw }}</span>
|
||||||
|
@endforeach
|
||||||
|
@if(count($jurusan->keywords ?? []) > 5)
|
||||||
|
<span class="inline-block px-2 py-0.5 rounded bg-gray-100 text-gray-500 text-xs">+{{ count($jurusan->keywords) - 5 }}</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 hidden lg:table-cell">
|
||||||
|
<div class="flex flex-wrap gap-1">
|
||||||
|
@foreach($jurusan->preferensi_studi ?? [] as $ps)
|
||||||
|
<span class="inline-block px-2 py-0.5 rounded bg-green-100 text-green-700 text-xs">{{ $ps }}</span>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-center">
|
||||||
|
<div class="flex items-center justify-center gap-2">
|
||||||
|
<a href="{{ route('bk.jurusan.edit', $jurusan->id) }}" class="px-3 py-1 bg-blue-100 text-blue-700 rounded text-xs font-semibold hover:bg-blue-200 transition">
|
||||||
|
✏️ Edit
|
||||||
|
</a>
|
||||||
|
<form action="{{ route('bk.jurusan.destroy', $jurusan->id) }}" method="POST" onsubmit="return confirm('Yakin ingin menghapus jurusan {{ $jurusan->nama_jurusan }}?')">
|
||||||
|
@csrf
|
||||||
|
@method('DELETE')
|
||||||
|
<button type="submit" class="px-3 py-1 bg-red-100 text-red-700 rounded text-xs font-semibold hover:bg-red-200 transition">
|
||||||
|
🗑️ Hapus
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="px-4 py-6 text-center text-gray-500">
|
||||||
|
Belum ada data jurusan. <a href="{{ route('bk.jurusan.create') }}" class="text-bk font-semibold hover:underline">Tambah jurusan pertama</a>.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Variabel Input Info -->
|
||||||
|
<div class="mt-8 bg-white rounded-lg shadow p-6">
|
||||||
|
<h3 class="text-lg font-bold text-bk mb-4">📊 Kriteria Penilaian Rekomendasi</h3>
|
||||||
|
<p class="text-gray-700 text-sm mb-4">Sistem menggunakan 5 kriteria utama untuk memberikan rekomendasi jurusan yang tepat:</p>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
<div class="p-4 bg-blue-50 rounded-lg border-l-4 border-blue-400">
|
||||||
|
<p class="font-bold text-blue-800 text-sm">📝 Nilai Akademik (40%)</p>
|
||||||
|
<p class="text-xs text-blue-700 mt-1">IPA: MTK, Fisika, Kimia, Biologi<br>IPS: Ekonomi, Geografi, Sosiologi, Sejarah</p>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 bg-green-50 rounded-lg border-l-4 border-green-400">
|
||||||
|
<p class="font-bold text-green-800 text-sm">💡 Minat & Bakat (35%)</p>
|
||||||
|
<p class="text-xs text-green-700 mt-1">Dicocokkan dengan keywords jurusan secara graduated</p>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 bg-yellow-50 rounded-lg border-l-4 border-yellow-400">
|
||||||
|
<p class="font-bold text-yellow-800 text-sm">🎯 Preferensi Studi (15%)</p>
|
||||||
|
<p class="text-xs text-yellow-700 mt-1">Praktik Langsung, DuDi, Project Based, Blended Learning</p>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 bg-purple-50 rounded-lg border-l-4 border-purple-400">
|
||||||
|
<p class="font-bold text-purple-800 text-sm">🏆 Prestasi (5%)</p>
|
||||||
|
<p class="text-xs text-purple-700 mt-1">Prestasi akademik dan non-akademik siswa</p>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 bg-red-50 rounded-lg border-l-4 border-red-400">
|
||||||
|
<p class="font-bold text-red-800 text-sm">💼 Cita-cita (5%)</p>
|
||||||
|
<p class="text-xs text-red-700 mt-1">Dicocokkan dengan keywords jurusan secara graduated</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
@ -0,0 +1,263 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="id">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||||
|
<title>@yield('title', 'Panel Guru BK') - SPK Jurusan Kuliah</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
.gradient-bk { background: linear-gradient(135deg, #0f766e 0%, #14b8a6 100%); }
|
||||||
|
.text-bk { color: #0f766e; }
|
||||||
|
.border-bk { border-color: #0f766e; }
|
||||||
|
.bg-cream { background-color: #F8FAFC; }
|
||||||
|
.stat-card { transition: all 0.3s ease; }
|
||||||
|
.stat-card:hover { transform: translateY(-5px); box-shadow: 0 5px 15px rgba(15, 118, 110, 0.1); }
|
||||||
|
|
||||||
|
/* Sidebar */
|
||||||
|
.sidebar-dark {
|
||||||
|
background: linear-gradient(180deg, #1e293b 0%, #0f172a 100%);
|
||||||
|
}
|
||||||
|
.sidebar-link {
|
||||||
|
transition: all 0.25s cubic-bezier(.4,0,.2,1);
|
||||||
|
border-left: 3px solid transparent;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
.sidebar-link:hover {
|
||||||
|
background: rgba(20, 184, 166, 0.12);
|
||||||
|
color: #e2e8f0;
|
||||||
|
border-left-color: rgba(20, 184, 166, 0.5);
|
||||||
|
}
|
||||||
|
.sidebar-link.active {
|
||||||
|
background: linear-gradient(90deg, rgba(20,184,166,0.2) 0%, rgba(20,184,166,0.03) 100%);
|
||||||
|
color: #5eead4 !important;
|
||||||
|
border-left-color: #5eead4;
|
||||||
|
}
|
||||||
|
.sidebar-link .sidebar-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(255,255,255,0.06);
|
||||||
|
font-size: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
}
|
||||||
|
.sidebar-link:hover .sidebar-icon {
|
||||||
|
background: rgba(20, 184, 166, 0.25);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
.sidebar-link.active .sidebar-icon {
|
||||||
|
background: rgba(94, 234, 212, 0.15);
|
||||||
|
}
|
||||||
|
.sidebar-section-label {
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #475569;
|
||||||
|
padding: 0 1rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.sidebar-brand {
|
||||||
|
padding: 1.25rem 1rem;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.06);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.sidebar-brand-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background: linear-gradient(135deg, #0f766e 0%, #14b8a6 100%);
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 20px;
|
||||||
|
box-shadow: 0 4px 12px rgba(15, 118, 110, 0.3);
|
||||||
|
}
|
||||||
|
.sidebar-footer {
|
||||||
|
border-top: 1px solid rgba(255,255,255,0.06);
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.sidebar-desktop { display: none; }
|
||||||
|
.sidebar-mobile.open { display: block; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@yield('styles')
|
||||||
|
</head>
|
||||||
|
<body class="bg-cream">
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="gradient-bk text-white shadow-lg sticky top-0 z-50">
|
||||||
|
<div class="container mx-auto px-4 sm:px-6 py-4">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<button id="mobileMenuBtn" class="md:hidden text-white focus:outline-none">
|
||||||
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div>
|
||||||
|
<h1 class="text-lg sm:text-xl md:text-2xl font-bold">📋 Panel Guru BK</h1>
|
||||||
|
<p class="text-xs text-teal-100 font-semibold">Sistem Pemilihan Jurusan Kuliah</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="relative">
|
||||||
|
<button id="profileDropdownBtn" class="bg-white font-bold py-2 px-4 rounded-lg hover:bg-gray-100 transition text-xs sm:text-sm flex items-center gap-2 text-bk">
|
||||||
|
👤 {{ Auth::user()->name }}
|
||||||
|
<svg id="dropdownArrow" class="w-4 h-4 transition-transform duration-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div id="profileDropdown" class="absolute right-0 mt-2 w-48 bg-white text-gray-800 rounded-lg shadow-lg hidden z-50">
|
||||||
|
<a href="{{ route('bk.profil') }}" class="block px-4 py-3 hover:bg-gray-50 text-xs sm:text-sm font-semibold border-b">
|
||||||
|
👤 Profil Saya
|
||||||
|
</a>
|
||||||
|
<form method="POST" action="{{ route('logout') }}">
|
||||||
|
@csrf
|
||||||
|
<button type="submit" class="block w-full text-left px-4 py-3 hover:bg-gray-50 text-xs sm:text-sm font-semibold text-red-600 rounded-b-lg">
|
||||||
|
🚪 Logout
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="flex min-h-screen">
|
||||||
|
<!-- Sidebar Desktop -->
|
||||||
|
<aside class="hidden md:block w-64 sidebar-dark shadow-2xl flex-shrink-0">
|
||||||
|
<div class="sticky top-20">
|
||||||
|
<div class="sidebar-brand">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="sidebar-brand-icon">📋</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-white font-bold text-sm leading-tight">SPK Jurusan</p>
|
||||||
|
<p class="text-xs text-slate-400">Panel Guru BK</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="px-3 py-2 space-y-1">
|
||||||
|
<p class="sidebar-section-label mt-2">Menu Utama</p>
|
||||||
|
|
||||||
|
<a href="{{ route('bk.dashboard') }}" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium {{ request()->routeIs('bk.dashboard') ? 'active' : '' }}">
|
||||||
|
<span class="sidebar-icon">📊</span> Dashboard
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('bk.students') }}" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium {{ request()->routeIs('bk.students*') ? 'active' : '' }}">
|
||||||
|
<span class="sidebar-icon">👥</span> Data Siswa
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('bk.riwayat-rekomendasi') }}" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium {{ request()->routeIs('bk.riwayat-rekomendasi*') ? 'active' : '' }}">
|
||||||
|
<span class="sidebar-icon">🎯</span> Hasil Rekomendasi
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('bk.riwayat-chatbot') }}" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium {{ request()->routeIs('bk.riwayat-chatbot*') ? 'active' : '' }}">
|
||||||
|
<span class="sidebar-icon">💬</span> Riwayat Konsultasi
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<p class="sidebar-section-label mt-5">Kelola</p>
|
||||||
|
|
||||||
|
<a href="{{ route('bk.jurusan') }}" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium {{ request()->routeIs('bk.jurusan*') ? 'active' : '' }}">
|
||||||
|
<span class="sidebar-icon">🏛️</span> Manajemen Jurusan
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="sidebar-footer mt-4">
|
||||||
|
<div class="flex items-center gap-3 px-3 py-2">
|
||||||
|
<div class="w-8 h-8 rounded-full bg-gradient-to-br from-teal-400 to-emerald-300 flex items-center justify-center text-white text-xs font-bold">
|
||||||
|
{{ strtoupper(substr(Auth::user()->name, 0, 1)) }}
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<p class="text-xs font-medium text-slate-300 truncate">{{ Auth::user()->name }}</p>
|
||||||
|
<p class="text-xs text-slate-500">Guru BK</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Mobile Sidebar Overlay -->
|
||||||
|
<div id="mobileSidebar" class="fixed inset-0 z-40 hidden">
|
||||||
|
<div class="absolute inset-0 bg-black bg-opacity-60 backdrop-blur-sm" id="mobileOverlay"></div>
|
||||||
|
<aside class="relative w-72 h-full sidebar-dark shadow-2xl overflow-y-auto">
|
||||||
|
<div class="p-4 flex justify-between items-center" style="border-bottom: 1px solid rgba(255,255,255,0.06);">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="sidebar-brand-icon" style="width:36px;height:36px;font-size:18px;">📋</div>
|
||||||
|
<span class="font-bold text-white text-sm">Panel Guru BK</span>
|
||||||
|
</div>
|
||||||
|
<button id="closeMobileMenu" class="text-slate-400 hover:text-white transition text-xl">✕</button>
|
||||||
|
</div>
|
||||||
|
<nav class="px-3 py-3 space-y-1">
|
||||||
|
<p class="sidebar-section-label mt-1">Menu Utama</p>
|
||||||
|
<a href="{{ route('bk.dashboard') }}" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium {{ request()->routeIs('bk.dashboard') ? 'active' : '' }}">
|
||||||
|
<span class="sidebar-icon">📊</span> Dashboard
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('bk.students') }}" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium {{ request()->routeIs('bk.students*') ? 'active' : '' }}">
|
||||||
|
<span class="sidebar-icon">👥</span> Data Siswa
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('bk.riwayat-rekomendasi') }}" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium {{ request()->routeIs('bk.riwayat-rekomendasi*') ? 'active' : '' }}">
|
||||||
|
<span class="sidebar-icon">🎯</span> Hasil Rekomendasi
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('bk.riwayat-chatbot') }}" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium {{ request()->routeIs('bk.riwayat-chatbot*') ? 'active' : '' }}">
|
||||||
|
<span class="sidebar-icon">💬</span> Riwayat Konsultasi
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<p class="sidebar-section-label mt-5">Kelola</p>
|
||||||
|
<a href="{{ route('bk.jurusan') }}" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium {{ request()->routeIs('bk.jurusan*') ? 'active' : '' }}">
|
||||||
|
<span class="sidebar-icon">🏛️</span> Manajemen Jurusan
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main class="flex-1 p-4 sm:p-6 lg:p-8 overflow-x-hidden">
|
||||||
|
@if(session('success'))
|
||||||
|
<div class="mb-4 p-4 bg-green-100 border-l-4 border-green-500 text-green-700 rounded">
|
||||||
|
<p class="font-semibold">✅ {{ session('success') }}</p>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
@if(session('error'))
|
||||||
|
<div class="mb-4 p-4 bg-red-100 border-l-4 border-red-500 text-red-700 rounded">
|
||||||
|
<p class="font-semibold">❌ {{ session('error') }}</p>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@yield('content')
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Profile dropdown
|
||||||
|
const profileDropdownBtn = document.getElementById('profileDropdownBtn');
|
||||||
|
const profileDropdown = document.getElementById('profileDropdown');
|
||||||
|
const dropdownArrow = document.getElementById('dropdownArrow');
|
||||||
|
|
||||||
|
profileDropdownBtn.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
profileDropdown.classList.toggle('hidden');
|
||||||
|
dropdownArrow.style.transform = profileDropdown.classList.contains('hidden') ? 'rotate(0deg)' : 'rotate(180deg)';
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
if (!profileDropdownBtn.contains(e.target) && !profileDropdown.contains(e.target)) {
|
||||||
|
profileDropdown.classList.add('hidden');
|
||||||
|
dropdownArrow.style.transform = 'rotate(0deg)';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mobile sidebar
|
||||||
|
const mobileMenuBtn = document.getElementById('mobileMenuBtn');
|
||||||
|
const mobileSidebar = document.getElementById('mobileSidebar');
|
||||||
|
const mobileOverlay = document.getElementById('mobileOverlay');
|
||||||
|
const closeMobileMenu = document.getElementById('closeMobileMenu');
|
||||||
|
|
||||||
|
mobileMenuBtn.addEventListener('click', () => mobileSidebar.classList.remove('hidden'));
|
||||||
|
mobileOverlay.addEventListener('click', () => mobileSidebar.classList.add('hidden'));
|
||||||
|
closeMobileMenu.addEventListener('click', () => mobileSidebar.classList.add('hidden'));
|
||||||
|
</script>
|
||||||
|
@yield('scripts')
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
@extends('bk.layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'Profil Guru BK')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="mb-6">
|
||||||
|
<h2 class="text-2xl font-bold text-bk">⚙️ Profil Guru BK</h2>
|
||||||
|
<p class="text-sm text-gray-500 mt-1">Kelola informasi akun Guru BK</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if(session('success'))
|
||||||
|
<div class="bg-green-50 border-l-4 border-green-400 p-4 rounded-lg mb-6">
|
||||||
|
<p class="text-green-800 text-sm font-semibold">✅ {{ session('success') }}</p>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if(session('error'))
|
||||||
|
<div class="bg-red-50 border-l-4 border-red-400 p-4 rounded-lg mb-6">
|
||||||
|
<p class="text-red-800 text-sm font-semibold">❌ {{ session('error') }}</p>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
<!-- Update Profile -->
|
||||||
|
<div class="bg-white rounded-lg shadow p-6">
|
||||||
|
<h3 class="text-lg font-bold text-bk mb-4">👤 Informasi Akun</h3>
|
||||||
|
<form action="{{ route('bk.profil.update') }}" method="POST" class="space-y-4">
|
||||||
|
@csrf
|
||||||
|
@method('PUT')
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-semibold text-gray-700 mb-2">Nama *</label>
|
||||||
|
<input type="text" name="name" required value="{{ old('name', $guru->name) }}" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-teal-500">
|
||||||
|
@error('name') <span class="text-red-500 text-xs">{{ $message }}</span> @enderror
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-semibold text-gray-700 mb-2">Email *</label>
|
||||||
|
<input type="email" name="email" required value="{{ old('email', $guru->email) }}" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-teal-500">
|
||||||
|
@error('email') <span class="text-red-500 text-xs">{{ $message }}</span> @enderror
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-semibold text-gray-700 mb-2">Role</label>
|
||||||
|
<input type="text" value="Guru BK" readonly class="w-full px-4 py-2 border border-gray-200 rounded-lg bg-gray-100 text-gray-500 cursor-not-allowed">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-semibold text-gray-700 mb-2">Terdaftar Sejak</label>
|
||||||
|
<input type="text" value="{{ $guru->created_at->format('d M Y H:i') }}" readonly class="w-full px-4 py-2 border border-gray-200 rounded-lg bg-gray-100 text-gray-500 cursor-not-allowed">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="w-full gradient-bk text-white font-bold py-3 px-4 rounded-lg hover:opacity-90 transition">
|
||||||
|
💾 Update Profil
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Change Password -->
|
||||||
|
<div class="bg-white rounded-lg shadow p-6">
|
||||||
|
<h3 class="text-lg font-bold text-bk mb-4">🔒 Ubah Password</h3>
|
||||||
|
<form action="{{ route('bk.profil.password') }}" method="POST" class="space-y-4">
|
||||||
|
@csrf
|
||||||
|
@method('PUT')
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-semibold text-gray-700 mb-2">Password Lama *</label>
|
||||||
|
<div style="position: relative; display: flex; align-items: center;">
|
||||||
|
<input type="password" id="currentPass" name="current_password" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-teal-400" placeholder="Masukkan password lama" style="padding-right: 45px;">
|
||||||
|
<button type="button" style="position: absolute; right: 12px; background: none; border: none; cursor: pointer; color: #0f766e; font-size: 18px;" onclick="togglePasswordVisibility('currentPass', this)">👁️</button>
|
||||||
|
</div>
|
||||||
|
@error('current_password') <span class="text-red-500 text-xs">{{ $message }}</span> @enderror
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-semibold text-gray-700 mb-2">Password Baru *</label>
|
||||||
|
<div style="position: relative; display: flex; align-items: center;">
|
||||||
|
<input type="password" id="newPass" name="password" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-teal-400" placeholder="Minimal 8 karakter" style="padding-right: 45px;">
|
||||||
|
<button type="button" style="position: absolute; right: 12px; background: none; border: none; cursor: pointer; color: #0f766e; font-size: 18px;" onclick="togglePasswordVisibility('newPass', this)">👁️</button>
|
||||||
|
</div>
|
||||||
|
@error('password') <span class="text-red-500 text-xs">{{ $message }}</span> @enderror
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-semibold text-gray-700 mb-2">Konfirmasi Password Baru *</label>
|
||||||
|
<div style="position: relative; display: flex; align-items: center;">
|
||||||
|
<input type="password" id="newPassConfirm" name="password_confirmation" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-teal-400" placeholder="Ulangi password baru" style="padding-right: 45px;">
|
||||||
|
<button type="button" style="position: absolute; right: 12px; background: none; border: none; cursor: pointer; color: #0f766e; font-size: 18px;" onclick="togglePasswordVisibility('newPassConfirm', this)">👁️</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="w-full bg-teal-600 text-white font-bold py-3 px-4 rounded-lg hover:bg-teal-700 transition">
|
||||||
|
🔑 Ubah Password
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('scripts')
|
||||||
|
<script>
|
||||||
|
function togglePasswordVisibility(inputId, buttonElement) {
|
||||||
|
const input = document.getElementById(inputId);
|
||||||
|
const isPassword = input.type === 'password';
|
||||||
|
input.type = isPassword ? 'text' : 'password';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
@extends('bk.layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'Riwayat Konsultasi Chatbot')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-6">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-bold text-bk">💬 Riwayat Konsultasi Chatbot</h2>
|
||||||
|
<p class="text-sm text-gray-500 mt-1">Seluruh riwayat konsultasi siswa dengan AI Konselor BK</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Stats Cards -->
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6">
|
||||||
|
<div class="bg-white rounded-lg shadow p-4 border-t-4 border-teal-500 stat-card">
|
||||||
|
<p class="text-gray-600 text-sm font-semibold">Total Percakapan</p>
|
||||||
|
<p class="text-2xl font-bold text-bk mt-1">{{ $chatHistories->total() }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white rounded-lg shadow p-4 border-t-4 border-blue-400 stat-card">
|
||||||
|
<p class="text-gray-600 text-sm font-semibold">Siswa Unik</p>
|
||||||
|
<p class="text-2xl font-bold text-blue-600 mt-1">{{ $uniqueStudents }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white rounded-lg shadow p-4 border-t-4 border-green-400 stat-card">
|
||||||
|
<p class="text-gray-600 text-sm font-semibold">Hari Ini</p>
|
||||||
|
<p class="text-2xl font-bold text-green-600 mt-1">{{ $todayCount }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter -->
|
||||||
|
<div class="bg-white rounded-lg shadow p-4 mb-6 border-l-4 border-teal-500">
|
||||||
|
<form method="GET" class="flex gap-3 flex-col sm:flex-row">
|
||||||
|
<input type="text" name="search" placeholder="Cari nama siswa atau isi percakapan..." class="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-teal-500" value="{{ request('search') }}">
|
||||||
|
<button type="submit" class="gradient-bk text-white font-bold px-6 py-2 rounded-lg hover:opacity-90 transition">
|
||||||
|
🔍 Cari
|
||||||
|
</button>
|
||||||
|
@if(request('search'))
|
||||||
|
<a href="{{ route('bk.riwayat-chatbot') }}" class="bg-gray-400 text-white font-bold px-4 py-2 rounded-lg hover:bg-gray-500 transition text-center">
|
||||||
|
Reset
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Table -->
|
||||||
|
<div class="bg-white rounded-lg shadow overflow-x-auto">
|
||||||
|
<table class="w-full text-sm">
|
||||||
|
<thead class="gradient-bk text-white">
|
||||||
|
<tr>
|
||||||
|
<th class="px-4 py-3 text-left">No</th>
|
||||||
|
<th class="px-4 py-3 text-left">Nama Siswa</th>
|
||||||
|
<th class="px-4 py-3 text-left">Pertanyaan</th>
|
||||||
|
<th class="px-4 py-3 text-left">Jawaban AI</th>
|
||||||
|
<th class="px-4 py-3 text-center">Tanggal</th>
|
||||||
|
<th class="px-4 py-3 text-center">Aksi</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y">
|
||||||
|
@forelse($chatHistories as $idx => $chat)
|
||||||
|
<tr class="hover:bg-gray-50 transition">
|
||||||
|
<td class="px-4 py-3 text-gray-600">{{ $chatHistories->firstItem() + $idx }}</td>
|
||||||
|
<td class="px-4 py-3 font-semibold text-gray-800">{{ $chat->user->name ?? 'Deleted User' }}</td>
|
||||||
|
<td class="px-4 py-3 text-gray-600 text-xs max-w-xs">
|
||||||
|
<div class="bg-blue-50 border-l-2 border-blue-400 p-2 rounded">
|
||||||
|
{{ \Illuminate\Support\Str::limit($chat->prompt, 80) }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-gray-600 text-xs max-w-xs">
|
||||||
|
<div class="bg-green-50 border-l-2 border-green-400 p-2 rounded">
|
||||||
|
{{ \Illuminate\Support\Str::limit($chat->response, 80) }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-center text-gray-500 text-xs">{{ $chat->created_at->format('d M Y H:i') }}</td>
|
||||||
|
<td class="px-4 py-3 text-center">
|
||||||
|
<a href="{{ route('bk.student.detail', $chat->user_id) }}" class="text-teal-600 hover:text-teal-800 font-semibold text-xs">👁 Detail Siswa</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr>
|
||||||
|
<td colspan="6" class="px-4 py-8 text-center text-gray-500">
|
||||||
|
Belum ada data konsultasi chatbot
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagination -->
|
||||||
|
<div class="mt-6">
|
||||||
|
{{ $chatHistories->withQueryString()->links() }}
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
@extends('bk.layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'Riwayat Rekomendasi Siswa')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-6">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-bold text-bk">🎯 Riwayat Rekomendasi Siswa</h2>
|
||||||
|
<p class="text-sm text-gray-500 mt-1">Seluruh hasil rekomendasi jurusan yang pernah dilakukan siswa</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Stats Cards -->
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6">
|
||||||
|
<div class="bg-white rounded-lg shadow p-4 border-t-4 border-teal-500 stat-card">
|
||||||
|
<p class="text-gray-600 text-sm font-semibold">Total Rekomendasi</p>
|
||||||
|
<p class="text-2xl font-bold text-bk mt-1">{{ $recommendations->total() }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white rounded-lg shadow p-4 border-t-4 border-blue-400 stat-card">
|
||||||
|
<p class="text-gray-600 text-sm font-semibold">Siswa Unik</p>
|
||||||
|
<p class="text-2xl font-bold text-blue-600 mt-1">{{ $uniqueStudents }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white rounded-lg shadow p-4 border-t-4 border-green-400 stat-card">
|
||||||
|
<p class="text-gray-600 text-sm font-semibold">Jurusan Terpopuler</p>
|
||||||
|
<p class="text-lg font-bold text-green-600 mt-1">{{ $topMajor ?? '-' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter -->
|
||||||
|
<div class="bg-white rounded-lg shadow p-4 mb-6 border-l-4 border-teal-500">
|
||||||
|
<form method="GET" class="flex gap-3 flex-col sm:flex-row">
|
||||||
|
<input type="text" name="search" placeholder="Cari nama siswa..." class="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-teal-500" value="{{ request('search') }}">
|
||||||
|
<button type="submit" class="gradient-bk text-white font-bold px-6 py-2 rounded-lg hover:opacity-90 transition">
|
||||||
|
🔍 Cari
|
||||||
|
</button>
|
||||||
|
@if(request('search'))
|
||||||
|
<a href="{{ route('bk.riwayat-rekomendasi') }}" class="bg-gray-400 text-white font-bold px-4 py-2 rounded-lg hover:bg-gray-500 transition text-center">
|
||||||
|
Reset
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Table -->
|
||||||
|
<div class="bg-white rounded-lg shadow overflow-x-auto">
|
||||||
|
<table class="w-full text-sm">
|
||||||
|
<thead class="gradient-bk text-white">
|
||||||
|
<tr>
|
||||||
|
<th class="px-4 py-3 text-left">No</th>
|
||||||
|
<th class="px-4 py-3 text-left">Nama Siswa</th>
|
||||||
|
<th class="px-4 py-3 text-left">Kelompok</th>
|
||||||
|
<th class="px-4 py-3 text-left">Minat</th>
|
||||||
|
<th class="px-4 py-3 text-left">Top 3 Rekomendasi</th>
|
||||||
|
<th class="px-4 py-3 text-center">Tanggal</th>
|
||||||
|
<th class="px-4 py-3 text-center">Aksi</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y">
|
||||||
|
@forelse($recommendations as $idx => $rec)
|
||||||
|
<tr class="hover:bg-gray-50 transition">
|
||||||
|
<td class="px-4 py-3 text-gray-600">{{ $recommendations->firstItem() + $idx }}</td>
|
||||||
|
<td class="px-4 py-3 font-semibold text-gray-800">{{ $rec->user->name ?? 'Deleted User' }}</td>
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
@if($rec->user && $rec->user->kelompok_asal)
|
||||||
|
<span class="px-2 py-1 rounded text-xs font-bold" style="{{ $rec->user->kelompok_asal == 'IPA' ? 'background-color: #E0F2FE; color: #0369A1;' : 'background-color: #FEF3C7; color: #92400E;' }}">
|
||||||
|
{{ $rec->user->kelompok_asal }}
|
||||||
|
</span>
|
||||||
|
@else
|
||||||
|
<span class="text-gray-400">-</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-gray-600 text-xs">{{ \Illuminate\Support\Str::limit($rec->minat, 30) }}</td>
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
@if($rec->hasil_rekomendasi && is_array($rec->hasil_rekomendasi))
|
||||||
|
<div class="space-y-1">
|
||||||
|
@foreach(array_slice($rec->hasil_rekomendasi, 0, 3) as $i => $hasil)
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="w-5 h-5 flex items-center justify-center rounded-full text-xs font-bold {{ $i === 0 ? 'bg-yellow-400 text-yellow-900' : 'bg-gray-200 text-gray-600' }}">{{ $i + 1 }}</span>
|
||||||
|
<span class="text-xs text-gray-700">{{ $hasil['jurusan'] ?? 'N/A' }}</span>
|
||||||
|
@php $skorVal = $hasil['skor'] ?? 0; @endphp
|
||||||
|
<span class="text-xs font-semibold {{ $i === 0 ? 'text-teal-600' : 'text-gray-400' }}">{{ round(($skorVal > 1 ? $skorVal : $skorVal * 100), 1) }}%</span>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<span class="text-gray-400 text-xs">-</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-center text-gray-500 text-xs">{{ $rec->created_at->format('d M Y H:i') }}</td>
|
||||||
|
<td class="px-4 py-3 text-center">
|
||||||
|
<a href="{{ route('bk.student.detail', $rec->user_id) }}" class="text-teal-600 hover:text-teal-800 font-semibold text-xs">👁 Detail Siswa</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr>
|
||||||
|
<td colspan="7" class="px-4 py-8 text-center text-gray-500">
|
||||||
|
Belum ada data rekomendasi
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagination -->
|
||||||
|
<div class="mt-6">
|
||||||
|
{{ $recommendations->withQueryString()->links() }}
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
@extends('bk.layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'Detail Siswa - ' . $student->name)
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-bold text-bk">👤 Detail Siswa</h2>
|
||||||
|
<p class="text-sm text-gray-500 mt-1">{{ $student->name }}</p>
|
||||||
|
</div>
|
||||||
|
<a href="{{ route('bk.students') }}" class="bg-gray-400 text-white font-bold py-2 px-4 rounded-lg hover:bg-gray-500 transition text-sm">
|
||||||
|
← Kembali
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Profile Card -->
|
||||||
|
<div class="bg-white rounded-lg shadow-lg p-6 mb-6 border-l-4 border-teal-500">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<p class="text-gray-600 text-sm font-semibold">Nama</p>
|
||||||
|
<p class="text-xl font-bold text-bk mt-1">{{ $student->name }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-gray-600 text-sm font-semibold">Email</p>
|
||||||
|
<p class="text-gray-800 mt-1">{{ $student->email }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-gray-600 text-sm font-semibold">NIS</p>
|
||||||
|
<p class="text-gray-800 font-semibold mt-1">{{ $student->nis ?? '-' }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-gray-600 text-sm font-semibold">Kelompok Asal</p>
|
||||||
|
@if($student->kelompok_asal)
|
||||||
|
<p class="mt-1">
|
||||||
|
<span class="px-3 py-1 rounded text-sm font-bold" style="{{ $student->kelompok_asal == 'IPA' ? 'background-color: #E0F2FE; color: #0369A1;' : 'background-color: #FEF3C7; color: #92400E;' }}">
|
||||||
|
{{ $student->kelompok_asal }}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
@else
|
||||||
|
<p class="text-gray-500 mt-1">-</p>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-gray-600 text-sm font-semibold">Foto Profil</p>
|
||||||
|
@if($student->foto)
|
||||||
|
<img src="{{ Storage::url($student->foto) }}" alt="{{ $student->name }}" class="w-24 h-24 rounded-lg object-cover mt-2 border-2 border-teal-500">
|
||||||
|
@else
|
||||||
|
<p class="text-gray-500 mt-1">-</p>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-gray-600 text-sm font-semibold">Terdaftar</p>
|
||||||
|
<p class="text-gray-800 mt-1">{{ $student->created_at->format('d M Y H:i') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Rekomendasi -->
|
||||||
|
<div class="bg-white rounded-lg shadow p-6 mb-6 border-l-4 border-green-400">
|
||||||
|
<h3 class="text-lg font-bold text-bk mb-4">🎯 Riwayat Rekomendasi ({{ count($recommendations) }})</h3>
|
||||||
|
|
||||||
|
@if($recommendations->isNotEmpty())
|
||||||
|
<div class="space-y-4">
|
||||||
|
@foreach($recommendations as $rec)
|
||||||
|
<div class="border border-gray-200 rounded-lg p-4 hover:bg-gray-50 transition">
|
||||||
|
<div class="flex justify-between items-start mb-3">
|
||||||
|
<p class="text-sm text-gray-500">{{ $rec->created_at->format('d M Y H:i') }}</p>
|
||||||
|
<span class="px-2 py-1 rounded text-xs font-bold bg-teal-100 text-teal-800">Rekomendasi #{{ $loop->index + 1 }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($rec->hasil_rekomendasi && is_array($rec->hasil_rekomendasi))
|
||||||
|
<div class="mt-3">
|
||||||
|
<p class="text-xs font-semibold text-gray-600 mb-2">Top 3 Rekomendasi:</p>
|
||||||
|
<div class="space-y-2">
|
||||||
|
@foreach(array_slice($rec->hasil_rekomendasi, 0, 3) as $idx => $hasil)
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-sm text-gray-700">{{ $idx + 1 }}. {{ $hasil['jurusan'] ?? 'N/A' }}</span>
|
||||||
|
@php $skorVal = $hasil['skor'] ?? 0; @endphp
|
||||||
|
<span class="px-2 py-1 rounded text-xs font-bold" style="background-color: {{ $idx === 0 ? '#CCFBF1' : '#F3F4F6' }}; color: {{ $idx === 0 ? '#0f766e' : '#6B7280' }};">
|
||||||
|
{{ round(($skorVal > 1 ? $skorVal : $skorVal * 100), 1) }}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="mt-3 p-3 bg-gray-50 rounded text-xs text-gray-600">
|
||||||
|
<p><strong>Minat:</strong> {{ $rec->minat ?? '-' }} | <strong>Cita-cita:</strong> {{ $rec->cita_cita ?? '-' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<p class="text-gray-500 text-sm">Siswa belum melakukan rekomendasi</p>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Chat History -->
|
||||||
|
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-blue-400">
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<h3 class="text-lg font-bold text-bk">💬 Chat History ({{ count($chatHistories) }})</h3>
|
||||||
|
@if(count($chatHistories) > 0)
|
||||||
|
<a href="{{ route('bk.student.chat', $student->id) }}" class="bg-teal-500 text-white font-semibold py-2 px-3 rounded-lg hover:bg-teal-600 transition text-xs">
|
||||||
|
Lihat Semua →
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($chatHistories->isNotEmpty())
|
||||||
|
<div class="space-y-3 max-h-96 overflow-y-auto">
|
||||||
|
@foreach($chatHistories as $chat)
|
||||||
|
<div class="border-b pb-3 last:border-b-0">
|
||||||
|
<div class="flex justify-between items-start mb-2">
|
||||||
|
<p class="text-xs font-semibold text-gray-600">{{ $chat->created_at->format('d M Y H:i') }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-blue-50 border-l-4 border-blue-400 p-3 rounded mb-2">
|
||||||
|
<p class="text-xs font-semibold text-gray-700 mb-1">👤 Pertanyaan Siswa:</p>
|
||||||
|
<p class="text-sm text-gray-800">{{ \Illuminate\Support\Str::limit($chat->prompt, 150) }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-green-50 border-l-4 border-green-400 p-3 rounded">
|
||||||
|
<p class="text-xs font-semibold text-gray-700 mb-1">🤖 Jawaban AI:</p>
|
||||||
|
<p class="text-sm text-gray-800">{{ \Illuminate\Support\Str::limit($chat->response, 150) }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<p class="text-gray-500 text-sm">Siswa belum melakukan chat dengan AI</p>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
@extends('bk.layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'Data Siswa')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-6">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-bold text-bk">👥 Data Siswa</h2>
|
||||||
|
<p class="text-sm text-gray-500 mt-1">Total: {{ $students->total() }} Siswa</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Search & Filter -->
|
||||||
|
<div class="bg-white rounded-lg shadow p-4 mb-6 border-l-4 border-teal-500">
|
||||||
|
<form method="GET" class="flex gap-3 flex-col sm:flex-row">
|
||||||
|
<input type="text" name="search" placeholder="Cari nama atau NIS..." class="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-teal-400" value="{{ request('search') }}">
|
||||||
|
<select name="kelompok" class="px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-teal-400">
|
||||||
|
<option value="">-- Semua Kelompok --</option>
|
||||||
|
<option value="IPA" {{ request('kelompok') == 'IPA' ? 'selected' : '' }}>IPA</option>
|
||||||
|
<option value="IPS" {{ request('kelompok') == 'IPS' ? 'selected' : '' }}>IPS</option>
|
||||||
|
</select>
|
||||||
|
<button type="submit" class="gradient-bk text-white font-bold px-6 py-2 rounded-lg hover:opacity-90 transition">
|
||||||
|
🔍 Cari
|
||||||
|
</button>
|
||||||
|
@if(request('search') || request('kelompok'))
|
||||||
|
<a href="{{ route('bk.students') }}" class="bg-gray-400 text-white font-bold px-4 py-2 rounded-lg hover:bg-gray-500 transition text-center">
|
||||||
|
Reset
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Students Table -->
|
||||||
|
<div class="bg-white rounded-lg shadow overflow-x-auto">
|
||||||
|
<table class="w-full text-sm">
|
||||||
|
<thead class="gradient-bk text-white">
|
||||||
|
<tr>
|
||||||
|
<th class="px-4 py-3 text-left">Nama</th>
|
||||||
|
<th class="px-4 py-3 text-left">Email</th>
|
||||||
|
<th class="px-4 py-3 text-center">NIS</th>
|
||||||
|
<th class="px-4 py-3 text-center">Kelompok</th>
|
||||||
|
<th class="px-4 py-3 text-center">Rekomendasi</th>
|
||||||
|
<th class="px-4 py-3 text-center">Chat</th>
|
||||||
|
<th class="px-4 py-3 text-center">Aksi</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y">
|
||||||
|
@forelse($students as $student)
|
||||||
|
<tr class="hover:bg-gray-50 transition">
|
||||||
|
<td class="px-4 py-3 font-semibold text-gray-800">{{ $student->name }}</td>
|
||||||
|
<td class="px-4 py-3 text-gray-600 text-xs">{{ $student->email }}</td>
|
||||||
|
<td class="px-4 py-3 text-center text-gray-600">{{ $student->nis ?? '-' }}</td>
|
||||||
|
<td class="px-4 py-3 text-center">
|
||||||
|
@if($student->kelompok_asal)
|
||||||
|
<span class="px-2 py-1 rounded text-xs font-bold" style="{{ $student->kelompok_asal == 'IPA' ? 'background-color: #E0F2FE; color: #0369A1;' : 'background-color: #FEF3C7; color: #92400E;' }}">
|
||||||
|
{{ $student->kelompok_asal }}
|
||||||
|
</span>
|
||||||
|
@else
|
||||||
|
<span class="text-gray-400">-</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-center">
|
||||||
|
<span class="px-3 py-1 rounded text-xs font-bold" style="{{ $student->recommendations_count > 0 ? 'background-color: #DBEAFE; color: #1e40af;' : 'background-color: #F3F4F6; color: #9CA3AF;' }}">
|
||||||
|
{{ $student->recommendations_count }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-center">
|
||||||
|
<span class="px-3 py-1 rounded text-xs font-bold" style="{{ $student->chat_histories_count > 0 ? 'background-color: #D1FAE5; color: #065F46;' : 'background-color: #F3F4F6; color: #9CA3AF;' }}">
|
||||||
|
{{ $student->chat_histories_count }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-center">
|
||||||
|
<a href="{{ route('bk.student.detail', $student->id) }}" class="text-teal-600 hover:text-teal-800 font-semibold text-xs">👁 Lihat</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr>
|
||||||
|
<td colspan="7" class="px-4 py-6 text-center text-gray-500">Tidak ada siswa yang ditemukan</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6">
|
||||||
|
{{ $students->withQueryString()->links() }}
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
@ -94,6 +94,9 @@
|
||||||
<p class="text-xs sm:text-sm text-yellow-300 font-semibold mt-1">Konseling Pemilihan Jurusan Kuliah</p>
|
<p class="text-xs sm:text-sm text-yellow-300 font-semibold mt-1">Konseling Pemilihan Jurusan Kuliah</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2 sm:gap-4 w-full sm:w-auto">
|
<div class="flex items-center gap-2 sm:gap-4 w-full sm:w-auto">
|
||||||
|
<a href="{{ route('chatbot.index') }}" class="block sm:inline-block flex-1 sm:flex-none text-center bg-white text-maroon font-bold py-2 px-3 sm:px-4 rounded-lg hover:bg-gray-100 transition text-xs sm:text-sm">
|
||||||
|
Sesi Baru
|
||||||
|
</a>
|
||||||
<a href="{{ url('/dashboard') }}" class="block sm:inline-block flex-1 sm:flex-none text-center bg-yellow-400 text-maroon font-bold py-2 px-3 sm:px-4 rounded-lg hover:bg-yellow-300 transition text-xs sm:text-sm">
|
<a href="{{ url('/dashboard') }}" class="block sm:inline-block flex-1 sm:flex-none text-center bg-yellow-400 text-maroon font-bold py-2 px-3 sm:px-4 rounded-lg hover:bg-yellow-300 transition text-xs sm:text-sm">
|
||||||
Kembali
|
Kembali
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -114,8 +117,17 @@
|
||||||
<div class="bg-green-50 border border-green-200 rounded-lg p-3">
|
<div class="bg-green-50 border border-green-200 rounded-lg p-3">
|
||||||
<p class="text-xs text-green-600 font-semibold">Rekomendasi Terakhir</p>
|
<p class="text-xs text-green-600 font-semibold">Rekomendasi Terakhir</p>
|
||||||
<p class="text-base sm:text-lg font-bold text-maroon">{{ $recommendation['jurusan'] }}</p>
|
<p class="text-base sm:text-lg font-bold text-maroon">{{ $recommendation['jurusan'] }}</p>
|
||||||
<p class="text-xs sm:text-sm text-gray-600">Skor: {{ isset($recommendation['skor']) ? number_format($recommendation['skor'] * 100, 1) : '-' }}%</p>
|
<p class="text-xs sm:text-sm text-gray-600">Skor: {{ isset($recommendation['skor']) ? number_format(($recommendation['skor'] > 1 ? $recommendation['skor'] : $recommendation['skor'] * 100), 1) : '-' }}%</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if(!empty($recommendation['top3']) && count($recommendation['top3']) > 1)
|
||||||
|
<div class="mt-2 text-xs text-gray-500">
|
||||||
|
<p class="font-semibold mb-1">Alternatif lain:</p>
|
||||||
|
@foreach(array_slice($recommendation['top3'], 1) as $alt)
|
||||||
|
<p>• {{ $alt['jurusan'] }} ({{ number_format(($alt['skor'] > 1 ? $alt['skor'] : $alt['skor'] * 100), 1) }}%)</p>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-3">
|
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-3">
|
||||||
|
|
@ -146,7 +158,7 @@
|
||||||
<div class="chat-container flex-1 mb-3 sm:mb-4" id="chatContainer">
|
<div class="chat-container flex-1 mb-3 sm:mb-4" id="chatContainer">
|
||||||
<div class="message ai">
|
<div class="message ai">
|
||||||
<div class="ai-msg">
|
<div class="ai-msg">
|
||||||
<p>Selamat datang. Saya adalah konselor BK virtual SMA Bima Ambulu. Saya siap membantu Anda dalam pemilihan jurusan kuliah, informasi prospek karier, maupun konsultasi lainnya terkait pendidikan tinggi. Silakan sampaikan pertanyaan Anda.</p>
|
<p id="initialGreeting"></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -178,6 +190,38 @@ class="gradient-maroon text-white font-bold py-2 px-4 sm:px-6 rounded-lg hover:o
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// Session ID for this chat
|
||||||
|
const sessionId = @json($sessionId);
|
||||||
|
const previousMessages = @json($previousMessages ?? []);
|
||||||
|
const recommendationId = @json($recommendationId ?? null);
|
||||||
|
|
||||||
|
// Langsung update URL agar refresh tetap di sesi ini
|
||||||
|
if (!window.location.search.includes('session=')) {
|
||||||
|
window.history.replaceState({}, '', '{{ route("chatbot.index") }}?session=' + sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time-aware greeting
|
||||||
|
(function() {
|
||||||
|
const hour = new Date().getHours();
|
||||||
|
let sapaan;
|
||||||
|
if (hour >= 3 && hour < 11) {
|
||||||
|
sapaan = 'Selamat pagi';
|
||||||
|
} else if (hour >= 11 && hour < 15) {
|
||||||
|
sapaan = 'Selamat siang';
|
||||||
|
} else if (hour >= 15 && hour < 18) {
|
||||||
|
sapaan = 'Selamat sore';
|
||||||
|
} else {
|
||||||
|
sapaan = 'Selamat malam';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previousMessages.length > 0) {
|
||||||
|
// Melanjutkan sesi lama — tampilkan info lanjutan
|
||||||
|
document.getElementById('initialGreeting').textContent = sapaan + '. Anda melanjutkan sesi konsultasi sebelumnya. Silakan lanjutkan pertanyaan Anda.';
|
||||||
|
} else {
|
||||||
|
document.getElementById('initialGreeting').textContent = sapaan + '. Saya adalah konselor BK virtual SMA Bima Ambulu. Saya siap membantu Anda dalam pemilihan jurusan kuliah, informasi prospek karier, maupun konsultasi lainnya terkait pendidikan tinggi. Silakan sampaikan pertanyaan Anda.';
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
const chatForm = document.getElementById('chatForm');
|
const chatForm = document.getElementById('chatForm');
|
||||||
const messageInput = document.getElementById('messageInput');
|
const messageInput = document.getElementById('messageInput');
|
||||||
const chatContainer = document.getElementById('chatContainer');
|
const chatContainer = document.getElementById('chatContainer');
|
||||||
|
|
@ -186,6 +230,14 @@ class="gradient-maroon text-white font-bold py-2 px-4 sm:px-6 rounded-lg hover:o
|
||||||
// Track conversation history for multi-turn context
|
// Track conversation history for multi-turn context
|
||||||
let conversationHistory = [];
|
let conversationHistory = [];
|
||||||
|
|
||||||
|
// Load previous messages if resuming session
|
||||||
|
if (previousMessages.length > 0) {
|
||||||
|
previousMessages.forEach(function(msg) {
|
||||||
|
addMessage(msg.text, msg.role === 'user' ? 'user' : 'ai');
|
||||||
|
conversationHistory.push({ role: msg.role, text: msg.text });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
chatForm.addEventListener('submit', async (e) => {
|
chatForm.addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
|
@ -202,18 +254,35 @@ class="gradient-maroon text-white font-bold py-2 px-4 sm:px-6 rounded-lg hover:o
|
||||||
conversationHistory.push({ role: 'user', text: message });
|
conversationHistory.push({ role: 'user', text: message });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Truncate history texts to avoid validation failure
|
||||||
|
const trimmedHistory = conversationHistory.slice(0, -1).map(h => ({
|
||||||
|
role: h.role,
|
||||||
|
text: h.text.substring(0, 1500)
|
||||||
|
}));
|
||||||
|
|
||||||
const response = await fetch('{{ route("chatbot.send") }}', {
|
const response = await fetch('{{ route("chatbot.send") }}', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
'X-CSRF-TOKEN': document.querySelector('input[name="_token"]').value
|
'X-CSRF-TOKEN': document.querySelector('input[name="_token"]').value
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
message: message,
|
message: message,
|
||||||
chatHistory: conversationHistory.slice(0, -1) // Send previous history (exclude current msg)
|
sessionId: sessionId,
|
||||||
|
recommendationId: recommendationId,
|
||||||
|
chatHistory: trimmedHistory
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => null);
|
||||||
|
const errorMsg = errorData?.message || 'Terjadi kesalahan pada server (kode: ' + response.status + '). Silakan coba lagi.';
|
||||||
|
addMessage(errorMsg, 'ai');
|
||||||
|
conversationHistory.pop(); // Remove failed user message from history
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,18 @@
|
||||||
color: #1f2937;
|
color: #1f2937;
|
||||||
border-radius: 12px 12px 12px 0;
|
border-radius: 12px 12px 12px 0;
|
||||||
}
|
}
|
||||||
|
.session-card {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
.session-card:hover {
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.chat-detail {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.chat-detail.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-cream">
|
<body class="bg-cream">
|
||||||
|
|
@ -35,10 +47,13 @@
|
||||||
<header class="gradient-maroon text-white shadow-lg sticky top-0 z-50">
|
<header class="gradient-maroon text-white shadow-lg sticky top-0 z-50">
|
||||||
<div class="container mx-auto px-4 sm:px-6 py-4 sm:py-6 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3 sm:gap-4">
|
<div class="container mx-auto px-4 sm:px-6 py-4 sm:py-6 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3 sm:gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-xl sm:text-2xl md:text-3xl font-bold">History Chat</h1>
|
<h1 class="text-xl sm:text-2xl md:text-3xl font-bold">Riwayat Konsultasi</h1>
|
||||||
<p class="text-xs sm:text-sm text-yellow-300 font-semibold mt-1">Riwayat Konsultasi AI</p>
|
<p class="text-xs sm:text-sm text-yellow-300 font-semibold mt-1">Arsip Sesi Konsultasi AI</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2 sm:gap-4 w-full sm:w-auto">
|
<div class="flex items-center gap-2 sm:gap-4 w-full sm:w-auto">
|
||||||
|
<a href="{{ route('chatbot.index') }}" class="block sm:inline-block flex-1 sm:flex-none text-center bg-white text-maroon font-bold py-2 px-3 sm:px-4 rounded-lg hover:bg-gray-100 transition text-xs sm:text-sm">
|
||||||
|
Konsultasi Baru
|
||||||
|
</a>
|
||||||
<a href="{{ url('/dashboard') }}" class="block sm:inline-block flex-1 sm:flex-none text-center bg-yellow-400 text-maroon font-bold py-2 px-3 sm:px-4 rounded-lg hover:bg-yellow-300 transition text-xs sm:text-sm">
|
<a href="{{ url('/dashboard') }}" class="block sm:inline-block flex-1 sm:flex-none text-center bg-yellow-400 text-maroon font-bold py-2 px-3 sm:px-4 rounded-lg hover:bg-yellow-300 transition text-xs sm:text-sm">
|
||||||
Kembali Dashboard
|
Kembali Dashboard
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -48,41 +63,72 @@
|
||||||
|
|
||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
<div class="container mx-auto px-4 sm:px-6 py-6 sm:py-12">
|
<div class="container mx-auto px-4 sm:px-6 py-6 sm:py-12">
|
||||||
@if($chatHistories && $chatHistories->count() > 0)
|
@if($sessions && $sessions->count() > 0)
|
||||||
<div class="space-y-4 sm:space-y-6">
|
<div class="space-y-4 sm:space-y-6">
|
||||||
<!-- Group by date -->
|
@foreach($sessions as $sessionKey => $session)
|
||||||
@php
|
<div class="session-card bg-white rounded-lg shadow-lg overflow-hidden border-l-4 border-maroon">
|
||||||
$groupedByDate = $chatHistories->groupBy(function($chat) {
|
<!-- Session Header (clickable) -->
|
||||||
return $chat->created_at->format('Y-m-d');
|
<div class="p-4 sm:p-6 cursor-pointer flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3"
|
||||||
});
|
onclick="toggleSession('session-{{ $loop->index }}')">
|
||||||
@endphp
|
<div class="flex-1 min-w-0">
|
||||||
|
<div class="flex items-center gap-2 mb-1">
|
||||||
|
<span class="text-lg">💬</span>
|
||||||
|
<h3 class="text-sm sm:text-base font-bold text-maroon truncate">
|
||||||
|
{{ $session['first_message'] }}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap items-center gap-2 sm:gap-4 text-xs text-gray-500">
|
||||||
|
<span>{{ $session['started_at']->format('d M Y, H:i') }}</span>
|
||||||
|
<span class="bg-gray-100 px-2 py-0.5 rounded-full">{{ $session['message_count'] }} pesan</span>
|
||||||
|
@if($session['started_at']->format('Y-m-d') !== $session['last_at']->format('Y-m-d'))
|
||||||
|
<span>s/d {{ $session['last_at']->format('d M Y, H:i') }}</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@if(!empty($session['recommendation']))
|
||||||
|
<div class="mt-2 flex items-center gap-2">
|
||||||
|
<span class="inline-flex items-center gap-1 bg-blue-50 border border-blue-200 text-blue-700 text-xs px-2 py-0.5 rounded-full">
|
||||||
|
📊 {{ $session['recommendation']['jurusan'] }}
|
||||||
|
({{ number_format(($session['recommendation']['skor'] > 1 ? $session['recommendation']['skor'] : $session['recommendation']['skor'] * 100), 1) }}%)
|
||||||
|
</span>
|
||||||
|
<span class="text-xs text-gray-400">
|
||||||
|
Rekomendasi {{ $session['recommendation']['tanggal']->format('d M Y') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 w-full sm:w-auto">
|
||||||
|
<a href="{{ route('chatbot.index', ['session' => $session['session_id']]) }}"
|
||||||
|
class="flex-1 sm:flex-none text-center gradient-maroon text-white font-bold py-2 px-3 sm:px-4 rounded-lg hover:opacity-90 transition text-xs sm:text-sm"
|
||||||
|
onclick="event.stopPropagation()">
|
||||||
|
Lanjutkan
|
||||||
|
</a>
|
||||||
|
<button class="flex-1 sm:flex-none text-center bg-gray-200 text-gray-700 font-bold py-2 px-3 sm:px-4 rounded-lg hover:bg-gray-300 transition text-xs sm:text-sm"
|
||||||
|
id="toggle-btn-{{ $loop->index }}">
|
||||||
|
Lihat Detail
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
@foreach($groupedByDate as $date => $chats)
|
<!-- Session Chat Detail (toggleable) -->
|
||||||
<div>
|
<div class="chat-detail border-t border-gray-200 p-4 sm:p-6 bg-gray-50" id="session-{{ $loop->index }}">
|
||||||
<h3 class="text-base sm:text-lg font-bold text-maroon mb-3 sm:mb-4">
|
<div class="space-y-3 sm:space-y-4 max-h-96 overflow-y-auto">
|
||||||
{{ \Carbon\Carbon::parse($date)->format('d F Y') }}
|
@foreach($session['chats'] as $chat)
|
||||||
</h3>
|
<!-- User Message -->
|
||||||
<div class="space-y-3 sm:space-y-4">
|
<div class="flex justify-end">
|
||||||
@foreach($chats as $chat)
|
<div class="user-message max-w-xs sm:max-w-md lg:max-w-lg xl:max-w-xl p-3 sm:p-4">
|
||||||
<div class="bg-white rounded-lg shadow-lg p-4 sm:p-6 border-l-4 border-maroon">
|
<p class="text-xs sm:text-sm">{{ $chat->prompt }}</p>
|
||||||
<div class="space-y-3 sm:space-y-4">
|
<p class="text-xs mt-2 opacity-75">{{ $chat->created_at->format('H:i') }}</p>
|
||||||
<!-- User Message -->
|
|
||||||
<div class="flex justify-end">
|
|
||||||
<div class="user-message max-w-xs sm:max-w-md lg:max-w-lg xl:max-w-xl p-3 sm:p-4">
|
|
||||||
<p class="text-xs sm:text-sm">{{ $chat->prompt }}</p>
|
|
||||||
<p class="text-xs mt-2 opacity-75">{{ $chat->created_at->format('H:i') }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- AI Response -->
|
|
||||||
<div class="flex justify-start">
|
|
||||||
<div class="ai-message max-w-xs sm:max-w-md lg:max-w-lg xl:max-w-xl p-3 sm:p-4">
|
|
||||||
<p class="text-xs sm:text-sm">{{ preg_replace(['/\*\*(.*?)\*\*/s', '/\*(.*?)\*/s', '/^#{1,6}\s+/m', '/`(.*?)`/s'], ['$1', '$1', '', '$1'], $chat->response) }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
@endforeach
|
<!-- AI Response -->
|
||||||
|
<div class="flex justify-start">
|
||||||
|
<div class="ai-message max-w-xs sm:max-w-md lg:max-w-lg xl:max-w-xl p-3 sm:p-4">
|
||||||
|
<p class="text-xs sm:text-sm">{{ preg_replace(['/\*\*(.*?)\*\*/s', '/\*(.*?)\*/s', '/^#{1,6}\s+/m', '/`(.*?)`/s'], ['$1', '$1', '', '$1'], $chat->response) }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endforeach
|
@endforeach
|
||||||
|
|
@ -90,10 +136,10 @@
|
||||||
@else
|
@else
|
||||||
<div class="bg-white rounded-lg shadow-lg p-8 sm:p-12 text-center">
|
<div class="bg-white rounded-lg shadow-lg p-8 sm:p-12 text-center">
|
||||||
<div class="text-5xl sm:text-6xl mb-4">💬</div>
|
<div class="text-5xl sm:text-6xl mb-4">💬</div>
|
||||||
<h3 class="text-xl sm:text-2xl font-bold text-maroon mb-2">Belum Ada Chat</h3>
|
<h3 class="text-xl sm:text-2xl font-bold text-maroon mb-2">Belum Ada Riwayat Konsultasi</h3>
|
||||||
<p class="text-xs sm:text-sm md:text-base text-gray-700 mb-6">Anda belum melakukan chat dengan AI. Mulai konsultasi sekarang!</p>
|
<p class="text-xs sm:text-sm md:text-base text-gray-700 mb-6">Anda belum melakukan konsultasi dengan AI. Mulai konsultasi sekarang!</p>
|
||||||
<a href="{{ url('/chatbot') }}" class="inline-block bg-maroon text-white font-bold py-2 sm:py-3 px-6 sm:px-8 rounded-lg hover:opacity-90 transition text-sm sm:text-base">
|
<a href="{{ route('chatbot.index') }}" class="inline-block gradient-maroon text-white font-bold py-2 sm:py-3 px-6 sm:px-8 rounded-lg hover:opacity-90 transition text-sm sm:text-base">
|
||||||
Mulai Chat
|
Mulai Konsultasi
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
@ -102,8 +148,24 @@
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<footer class="gradient-maroon text-white mt-8 sm:mt-12 py-4 sm:py-6">
|
<footer class="gradient-maroon text-white mt-8 sm:mt-12 py-4 sm:py-6">
|
||||||
<div class="container mx-auto px-4 sm:px-6 text-center">
|
<div class="container mx-auto px-4 sm:px-6 text-center">
|
||||||
<p class="text-xs sm:text-sm text-yellow-200">Sistem Pemilihan Jurusan © 2026 | SMA Bima Ambulu</p>
|
<p class="text-xs sm:text-sm text-yellow-200">Sistem Pemilihan Jurusan © 2026 | SMA Bima Ambulu</p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function toggleSession(sessionId) {
|
||||||
|
const detail = document.getElementById(sessionId);
|
||||||
|
const index = sessionId.replace('session-', '');
|
||||||
|
const btn = document.getElementById('toggle-btn-' + index);
|
||||||
|
|
||||||
|
if (detail.classList.contains('active')) {
|
||||||
|
detail.classList.remove('active');
|
||||||
|
if (btn) btn.textContent = 'Lihat Detail';
|
||||||
|
} else {
|
||||||
|
detail.classList.add('active');
|
||||||
|
if (btn) btn.textContent = 'Sembunyikan';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -58,19 +58,19 @@
|
||||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-2 sm:gap-4 mb-4 sm:mb-6">
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-2 sm:gap-4 mb-4 sm:mb-6">
|
||||||
<div class="bg-gray-50 p-3 sm:p-4 rounded-lg">
|
<div class="bg-gray-50 p-3 sm:p-4 rounded-lg">
|
||||||
<p class="text-xs sm:text-sm text-gray-600 font-semibold">Minat</p>
|
<p class="text-xs sm:text-sm text-gray-600 font-semibold">Minat</p>
|
||||||
<p class="text-sm sm:text-base font-bold text-maroon mt-1">{{ $rec->minat }}</p>
|
<p class="text-sm sm:text-base font-bold text-maroon mt-1">{{ $rec->minat ?? '-' }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-gray-50 p-3 sm:p-4 rounded-lg">
|
<div class="bg-gray-50 p-3 sm:p-4 rounded-lg">
|
||||||
<p class="text-xs sm:text-sm text-gray-600 font-semibold">Pref. Belajar</p>
|
<p class="text-xs sm:text-sm text-gray-600 font-semibold">Pref. Belajar</p>
|
||||||
<p class="text-sm sm:text-base font-bold text-maroon mt-1">{{ $rec->preferensi_studi }}</p>
|
<p class="text-sm sm:text-base font-bold text-maroon mt-1">{{ $rec->preferensi_studi ?? '-' }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-gray-50 p-3 sm:p-4 rounded-lg">
|
<div class="bg-gray-50 p-3 sm:p-4 rounded-lg">
|
||||||
<p class="text-xs sm:text-sm text-gray-600 font-semibold">Cita-Cita</p>
|
<p class="text-xs sm:text-sm text-gray-600 font-semibold">Cita-Cita</p>
|
||||||
<p class="text-sm sm:text-base font-bold text-maroon mt-1">{{ Str::limit($rec->cita_cita, 15) }}</p>
|
<p class="text-sm sm:text-base font-bold text-maroon mt-1">{{ Str::limit($rec->cita_cita ?? '-', 15) }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-gray-50 p-3 sm:p-4 rounded-lg">
|
<div class="bg-gray-50 p-3 sm:p-4 rounded-lg">
|
||||||
<p class="text-xs sm:text-sm text-gray-600 font-semibold">Prestasi</p>
|
<p class="text-xs sm:text-sm text-gray-600 font-semibold">Prestasi</p>
|
||||||
<p class="text-sm sm:text-base font-bold text-maroon mt-1">{{ Str::limit($rec->prestasi, 15) }}</p>
|
<p class="text-sm sm:text-base font-bold text-maroon mt-1">{{ Str::limit($rec->prestasi ?? '-', 15) }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -82,10 +82,11 @@
|
||||||
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2 sm:gap-4 p-3 sm:p-4 bg-white rounded-lg border border-yellow-300">
|
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2 sm:gap-4 p-3 sm:p-4 bg-white rounded-lg border border-yellow-300">
|
||||||
<div class="flex items-center gap-3 sm:gap-4">
|
<div class="flex items-center gap-3 sm:gap-4">
|
||||||
<span class="text-lg sm:text-xl font-bold text-yellow-500">{{ $index + 1 }}</span>
|
<span class="text-lg sm:text-xl font-bold text-yellow-500">{{ $index + 1 }}</span>
|
||||||
<span class="text-sm sm:text-base font-bold text-maroon">{{ $hasil['jurusan'] }}</span>
|
<span class="text-sm sm:text-base font-bold text-maroon">{{ $hasil['jurusan'] ?? '-' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-xs sm:text-sm bg-maroon text-white px-3 py-1 rounded-full font-bold">
|
<span class="text-xs sm:text-sm bg-maroon text-white px-3 py-1 rounded-full font-bold">
|
||||||
{{ number_format($hasil['skor'] * 100, 1) }}%
|
@php $skorVal = $hasil['skor'] ?? 0; @endphp
|
||||||
|
{{ number_format(($skorVal > 1 ? $skorVal : $skorVal * 100), 1) }}%
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@endforeach
|
@endforeach
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-yellow-50 p-3 sm:p-4 rounded-lg">
|
<div class="bg-yellow-50 p-3 sm:p-4 rounded-lg">
|
||||||
<p class="text-xs sm:text-sm text-gray-600">Skor Nilai</p>
|
<p class="text-xs sm:text-sm text-gray-600">Skor Nilai</p>
|
||||||
<p class="text-sm sm:text-lg font-bold text-maroon">{{ number_format($average / 100 * 100, 1) }}%</p>
|
<p class="text-sm sm:text-lg font-bold text-maroon">{{ number_format($average, 1) }}%</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -105,7 +105,7 @@
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm font-semibold text-gray-900">
|
<td class="px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm font-semibold text-gray-900">
|
||||||
{{ $res['jurusan'] }}
|
{{ $res['jurusan'] ?? '-' }}
|
||||||
@if($index == 0)
|
@if($index == 0)
|
||||||
<span class="ml-1 sm:ml-2 inline-block px-2 py-0.5 rounded text-xs font-semibold bg-yellow-100 text-yellow-800">
|
<span class="ml-1 sm:ml-2 inline-block px-2 py-0.5 rounded text-xs font-semibold bg-yellow-100 text-yellow-800">
|
||||||
⭐ Utama
|
⭐ Utama
|
||||||
|
|
@ -113,7 +113,7 @@
|
||||||
@endif
|
@endif
|
||||||
</td>
|
</td>
|
||||||
<td class="px-3 sm:px-6 py-2 sm:py-4 text-right text-xs sm:text-sm font-bold text-maroon">
|
<td class="px-3 sm:px-6 py-2 sm:py-4 text-right text-xs sm:text-sm font-bold text-maroon">
|
||||||
{{ number_format($res['skor'] * 100, 1) }}%
|
{{ number_format(($res['skor'] ?? 0) * 100, 1) }}%
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@endforeach
|
@endforeach
|
||||||
|
|
@ -126,11 +126,11 @@
|
||||||
@foreach($hasilAkhir as $index => $res)
|
@foreach($hasilAkhir as $index => $res)
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<span class="text-xs sm:text-sm font-semibold text-gray-700">{{ $res['jurusan'] }}</span>
|
<span class="text-xs sm:text-sm font-semibold text-gray-700">{{ $res['jurusan'] ?? '-' }}</span>
|
||||||
<span class="text-xs sm:text-sm font-bold text-maroon">{{ number_format($res['skor'] * 100, 1) }}%</span>
|
<span class="text-xs sm:text-sm font-bold text-maroon">{{ number_format(($res['skor'] ?? 0) * 100, 1) }}%</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full bg-gray-200 rounded-full h-2">
|
<div class="w-full bg-gray-200 rounded-full h-2">
|
||||||
<div class="gradient-maroon h-2 rounded-full" style="width: {{ number_format($res['skor'] * 100, 1) }}%"></div>
|
<div class="gradient-maroon h-2 rounded-full" style="width: {{ number_format(($res['skor'] ?? 0) * 100, 1) }}%"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endforeach
|
@endforeach
|
||||||
|
|
@ -148,9 +148,9 @@
|
||||||
<div class="flex flex-col sm:flex-row items-start gap-3 sm:gap-4 mb-4 sm:mb-6">
|
<div class="flex flex-col sm:flex-row items-start gap-3 sm:gap-4 mb-4 sm:mb-6">
|
||||||
<div class="w-12 h-12 sm:w-14 sm:h-14 rounded-lg bg-yellow-100 flex items-center justify-center text-xl sm:text-2xl flex-shrink-0">1</div>
|
<div class="w-12 h-12 sm:w-14 sm:h-14 rounded-lg bg-yellow-100 flex items-center justify-center text-xl sm:text-2xl flex-shrink-0">1</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-lg sm:text-2xl font-bold text-maroon mb-1 sm:mb-2">{{ $topRecommendation['jurusan'] }}</h3>
|
<h3 class="text-lg sm:text-2xl font-bold text-maroon mb-1 sm:mb-2">{{ $topRecommendation['jurusan'] ?? '-' }}</h3>
|
||||||
<p class="text-sm sm:text-lg text-gray-700">
|
<p class="text-sm sm:text-lg text-gray-700">
|
||||||
Skor Kesesuaian: <span class="font-bold text-maroon">{{ number_format($topRecommendation['skor'] * 100, 1) }}%</span>
|
Skor Kesesuaian: <span class="font-bold text-maroon">{{ number_format(($topRecommendation['skor'] ?? 0) * 100, 1) }}%</span>
|
||||||
</p>
|
</p>
|
||||||
@if($topJurusan && $topJurusan->deskripsi)
|
@if($topJurusan && $topJurusan->deskripsi)
|
||||||
<p class="text-xs sm:text-sm text-gray-600 mt-2">{{ $topJurusan->deskripsi }}</p>
|
<p class="text-xs sm:text-sm text-gray-600 mt-2">{{ $topJurusan->deskripsi }}</p>
|
||||||
|
|
@ -160,12 +160,12 @@
|
||||||
|
|
||||||
<!-- Analysis Breakdown -->
|
<!-- Analysis Breakdown -->
|
||||||
<div class="mb-4 sm:mb-6 p-3 sm:p-4 bg-gray-50 rounded-lg">
|
<div class="mb-4 sm:mb-6 p-3 sm:p-4 bg-gray-50 rounded-lg">
|
||||||
<h4 class="font-bold text-maroon text-sm sm:text-base mb-3 sm:mb-4">Analisis per Kriteria:</h4>
|
<h4 class="font-bold text-maroon text-sm sm:text-base mb-3 sm:mb-4">Likelihood per Kriteria (Weighted Naive Bayes):</h4>
|
||||||
|
|
||||||
<div class="space-y-2 sm:space-y-3">
|
<div class="space-y-2 sm:space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<div class="flex justify-between items-center mb-1">
|
<div class="flex justify-between items-center mb-1">
|
||||||
<p class="text-xs sm:text-sm font-semibold text-gray-700">Nilai Akademik (40%)</p>
|
<p class="text-xs sm:text-sm font-semibold text-gray-700">Nilai Akademik — P(nilai|H) × w=0.40</p>
|
||||||
<span class="text-xs sm:text-sm font-bold text-maroon">{{ number_format(($detail['nilai'] ?? 0) * 100, 1) }}%</span>
|
<span class="text-xs sm:text-sm font-bold text-maroon">{{ number_format(($detail['nilai'] ?? 0) * 100, 1) }}%</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full bg-gray-300 rounded-full h-2">
|
<div class="w-full bg-gray-300 rounded-full h-2">
|
||||||
|
|
@ -175,7 +175,7 @@
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="flex justify-between items-center mb-1">
|
<div class="flex justify-between items-center mb-1">
|
||||||
<p class="text-xs sm:text-sm font-semibold text-gray-700">Minat & Bakat (35%)</p>
|
<p class="text-xs sm:text-sm font-semibold text-gray-700">Minat & Bakat — P(minat|H) × w=0.35</p>
|
||||||
<span class="text-xs sm:text-sm font-bold text-maroon">{{ number_format(($detail['minat'] ?? 0) * 100, 1) }}%</span>
|
<span class="text-xs sm:text-sm font-bold text-maroon">{{ number_format(($detail['minat'] ?? 0) * 100, 1) }}%</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full bg-gray-300 rounded-full h-2">
|
<div class="w-full bg-gray-300 rounded-full h-2">
|
||||||
|
|
@ -185,7 +185,7 @@
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="flex justify-between items-center mb-1">
|
<div class="flex justify-between items-center mb-1">
|
||||||
<p class="text-xs sm:text-sm font-semibold text-gray-700">Preferensi Studi (15%)</p>
|
<p class="text-xs sm:text-sm font-semibold text-gray-700">Preferensi Studi — P(pref|H) × w=0.15</p>
|
||||||
<span class="text-xs sm:text-sm font-bold text-maroon">{{ number_format(($detail['pref'] ?? 0) * 100, 1) }}%</span>
|
<span class="text-xs sm:text-sm font-bold text-maroon">{{ number_format(($detail['pref'] ?? 0) * 100, 1) }}%</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full bg-gray-300 rounded-full h-2">
|
<div class="w-full bg-gray-300 rounded-full h-2">
|
||||||
|
|
@ -195,7 +195,7 @@
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="flex justify-between items-center mb-1">
|
<div class="flex justify-between items-center mb-1">
|
||||||
<p class="text-xs sm:text-sm font-semibold text-gray-700">Cita-cita (5%)</p>
|
<p class="text-xs sm:text-sm font-semibold text-gray-700">Cita-cita — P(cita|H) × w=0.05</p>
|
||||||
<span class="text-xs sm:text-sm font-bold text-maroon">{{ number_format(($detail['cita'] ?? 0) * 100, 1) }}%</span>
|
<span class="text-xs sm:text-sm font-bold text-maroon">{{ number_format(($detail['cita'] ?? 0) * 100, 1) }}%</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full bg-gray-300 rounded-full h-2">
|
<div class="w-full bg-gray-300 rounded-full h-2">
|
||||||
|
|
@ -205,7 +205,7 @@
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="flex justify-between items-center mb-1">
|
<div class="flex justify-between items-center mb-1">
|
||||||
<p class="text-xs sm:text-sm font-semibold text-gray-700">Prestasi (5%)</p>
|
<p class="text-xs sm:text-sm font-semibold text-gray-700">Prestasi — P(prestasi|H) × w=0.05</p>
|
||||||
<span class="text-xs sm:text-sm font-bold text-maroon">{{ number_format(($detail['prestasi'] ?? 0) * 100, 1) }}%</span>
|
<span class="text-xs sm:text-sm font-bold text-maroon">{{ number_format(($detail['prestasi'] ?? 0) * 100, 1) }}%</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full bg-gray-300 rounded-full h-2">
|
<div class="w-full bg-gray-300 rounded-full h-2">
|
||||||
|
|
@ -221,8 +221,8 @@
|
||||||
<p class="text-gray-700 text-xs sm:text-sm leading-relaxed">
|
<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>
|
Berdasarkan profil Anda dengan <strong>nilai akademik {{ $katNilai }} (rata-rata {{ number_format($average, 1) }})</strong>
|
||||||
dan <strong>preferensi studi {{ $prefStudi }}</strong>,
|
dan <strong>preferensi studi {{ $prefStudi }}</strong>,
|
||||||
sistem menganalisis bahwa <strong>{{ $topRecommendation['jurusan'] }}</strong>
|
sistem menganalisis bahwa <strong>{{ $topRecommendation['jurusan'] ?? '-' }}</strong>
|
||||||
adalah pilihan yang paling sesuai dengan skor {{ number_format($topRecommendation['skor'] * 100, 1) }}%.
|
adalah pilihan yang paling sesuai dengan skor {{ number_format(($topRecommendation['skor'] ?? 0) * 100, 1) }}%.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -243,11 +243,11 @@
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<h3 class="text-base sm:text-xl font-bold text-maroon mb-1 sm:mb-2">Konsultasi Lebih Lanjut</h3>
|
<h3 class="text-base sm:text-xl font-bold text-maroon mb-1 sm:mb-2">Konsultasi Lebih Lanjut</h3>
|
||||||
<p class="text-xs sm:text-sm md:text-base text-gray-700 mb-3 sm:mb-4">
|
<p class="text-xs sm:text-sm md:text-base text-gray-700 mb-3 sm:mb-4">
|
||||||
Masih ragu dengan hasil rekomendasi? Konsultasikan dengan AI untuk mendapatkan penjelasan lebih detail
|
Ingin tahu mengapa jurusan <strong>{{ $hasilAkhir[0]['jurusan'] ?? '' }}</strong> direkomendasikan?
|
||||||
tentang jurusan yang direkomendasikan atau tanyakan pertanyaan lainnya.
|
Konsultasikan dengan AI Konselor BK Virtual untuk penjelasan detail berdasarkan profil Anda.
|
||||||
</p>
|
</p>
|
||||||
<a href="{{ route('chatbot.index') }}" class="inline-block 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">
|
<a href="{{ route('chatbot.index', ['rec' => session('last_recommendation_id')]) }}" class="inline-block 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">
|
||||||
Konsultasi dengan AI
|
💬 Tanya AI: "Mengapa jurusan ini cocok untukku?"
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -266,7 +266,7 @@
|
||||||
<!-- 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">
|
||||||
<strong>Metode:</strong> Sistem menggunakan Graduated Scoring dengan 5 kriteria: Nilai Akademik (40%), Minat & Bakat (35%), Preferensi Studi (15%), Cita-cita (5%), Prestasi (5%). Setiap kriteria dihitung secara proporsional (0-100%) berdasarkan kecocokan keyword.
|
<strong>Metode:</strong> Sistem menggunakan algoritma Weighted Naive Bayes dengan 5 fitur berbobot: Nilai Akademik (w=0.40), Minat (w=0.35), Preferensi Studi (w=0.15), Cita-cita (w=0.05), Prestasi (w=0.05). Rumus: P(H|X) ∝ P(H) × ∏ P(Xi|H)<sup>wi</sup>, kemudian dinormalisasi menggunakan softmax.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -21,31 +21,41 @@
|
||||||
.mobile-menu-toggle {
|
.mobile-menu-toggle {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 767px) {
|
||||||
.mobile-menu-toggle {
|
.mobile-menu-toggle {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
.mobile-menu {
|
.mobile-menu {
|
||||||
display: none;
|
display: none !important;
|
||||||
|
flex-direction: column;
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: linear-gradient(135deg, #5B7B89 0%, #7B9BA5 100%);
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
gap: 0.5rem;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||||
|
z-index: 50;
|
||||||
}
|
}
|
||||||
.mobile-menu.active {
|
.mobile-menu.active {
|
||||||
display: block;
|
display: flex !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-cream">
|
<body class="bg-cream">
|
||||||
<header class="gradient-maroon text-white shadow-lg sticky top-0 z-50">
|
<header class="gradient-maroon text-white shadow-lg sticky top-0 z-50">
|
||||||
<div class="container mx-auto px-4 sm:px-6 py-4 flex justify-between items-center">
|
<div class="container mx-auto px-4 sm:px-6 py-4 flex justify-between items-center relative">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-xl sm:text-2xl md:text-3xl font-bold">Sistem Pemilihan Jurusan</h1>
|
<h1 class="text-xl sm:text-2xl md:text-3xl font-bold">Sistem Pemilihan Jurusan</h1>
|
||||||
<p class="text-xs sm:text-sm text-gray-200 font-semibold mt-1">Pilih Jurusan yang Tepat.</p>
|
<p class="text-xs sm:text-sm text-gray-200 font-semibold mt-1">Pilih Jurusan yang Tepat.</p>
|
||||||
</div>
|
</div>
|
||||||
<button class="mobile-menu-toggle text-white text-2xl" id="menuToggle">☰</button>
|
<button class="mobile-menu-toggle text-white text-2xl" id="menuToggle">☰</button>
|
||||||
<div class="mobile-menu hidden md:flex space-x-2 sm:space-x-4 absolute md:relative top-16 md:top-0 left-0 md:left-auto right-0 md:right-0 bg-gradient-maroon md:bg-transparent p-4 md:p-0 flex flex-col md:flex-row gap-2 md:gap-4 w-full md:w-auto">
|
<div class="mobile-menu md:flex md:space-x-4 md:items-center" id="mobileMenu">
|
||||||
@if (Route::has('login'))
|
@if (Route::has('login'))
|
||||||
<a href="{{ route('login') }}" class="bg-yellow-400 text-maroon font-bold py-2 px-4 sm:px-6 rounded-lg hover:bg-yellow-300 transition text-sm sm:text-base text-center">Login</a>
|
<a href="{{ route('login') }}" class="bg-yellow-400 text-maroon font-bold py-2 px-6 rounded-lg hover:bg-yellow-300 transition text-sm sm:text-base text-center block md:inline-block">Login</a>
|
||||||
<a href="{{ route('register') }}" class="border-2 border-yellow-400 text-yellow-300 font-bold py-2 px-4 sm:px-6 rounded-lg hover:bg-yellow-400 hover:text-maroon transition text-sm sm:text-base text-center">Daftar</a>
|
<a href="{{ route('register') }}" class="border-2 border-yellow-400 text-yellow-300 font-bold py-2 px-6 rounded-lg hover:bg-yellow-400 hover:text-maroon transition text-sm sm:text-base text-center block md:inline-block">Daftar</a>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -290,13 +300,23 @@
|
||||||
<script>
|
<script>
|
||||||
// Mobile menu toggle
|
// Mobile menu toggle
|
||||||
const menuToggle = document.getElementById('menuToggle');
|
const menuToggle = document.getElementById('menuToggle');
|
||||||
const mobileMenu = document.querySelector('.mobile-menu');
|
const mobileMenu = document.getElementById('mobileMenu');
|
||||||
|
|
||||||
if (menuToggle) {
|
if (menuToggle) {
|
||||||
menuToggle.addEventListener('click', function() {
|
menuToggle.addEventListener('click', function(e) {
|
||||||
|
e.stopPropagation();
|
||||||
mobileMenu.classList.toggle('active');
|
mobileMenu.classList.toggle('active');
|
||||||
|
menuToggle.textContent = mobileMenu.classList.contains('active') ? '✕' : '☰';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tutup menu saat klik di luar
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
if (mobileMenu && !mobileMenu.contains(e.target) && e.target !== menuToggle) {
|
||||||
|
mobileMenu.classList.remove('active');
|
||||||
|
menuToggle.textContent = '☰';
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
use App\Http\Controllers\RekomendasiController;
|
use App\Http\Controllers\RekomendasiController;
|
||||||
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 Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
|
@ -13,8 +14,8 @@
|
||||||
|
|
||||||
Route::get('/dashboard', function () {
|
Route::get('/dashboard', function () {
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
$recommendationCount = \App\Models\Recommendation::where('user_id', $user->id)->count();
|
$recommendationCount = $user ? \App\Models\Recommendation::where('user_id', $user->id)->count() : 0;
|
||||||
$chatCount = \App\Models\ChatHistory::where('user_id', $user->id)->count();
|
$chatCount = $user ? \App\Models\ChatHistory::where('user_id', $user->id)->count() : 0;
|
||||||
|
|
||||||
return view('dashboard', [
|
return view('dashboard', [
|
||||||
'recommendationCount' => $recommendationCount,
|
'recommendationCount' => $recommendationCount,
|
||||||
|
|
@ -79,4 +80,23 @@
|
||||||
Route::put('/profil/password', [AdminController::class, 'updatePassword'])->name('profil.password');
|
Route::put('/profil/password', [AdminController::class, 'updatePassword'])->name('profil.password');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// BK Routes (role-based access control)
|
||||||
|
Route::middleware(['auth', 'verified', 'isBK'])->prefix('bk')->name('bk.')->group(function () {
|
||||||
|
Route::get('/dashboard', [BKController::class, 'dashboard'])->name('dashboard');
|
||||||
|
Route::get('/students', [BKController::class, 'students'])->name('students');
|
||||||
|
Route::get('/students/{id}', [BKController::class, 'studentDetail'])->name('student.detail');
|
||||||
|
Route::get('/students/{id}/chat', [BKController::class, 'chatHistory'])->name('student.chat');
|
||||||
|
Route::get('/riwayat-rekomendasi', [BKController::class, 'riwayatRekomendasi'])->name('riwayat-rekomendasi');
|
||||||
|
Route::get('/riwayat-chatbot', [BKController::class, 'riwayatChatbot'])->name('riwayat-chatbot');
|
||||||
|
Route::get('/jurusan', [BKController::class, 'jurusan'])->name('jurusan');
|
||||||
|
Route::get('/jurusan/create', [BKController::class, 'jurusanCreate'])->name('jurusan.create');
|
||||||
|
Route::post('/jurusan', [BKController::class, 'jurusanStore'])->name('jurusan.store');
|
||||||
|
Route::get('/jurusan/{id}/edit', [BKController::class, 'jurusanEdit'])->name('jurusan.edit');
|
||||||
|
Route::put('/jurusan/{id}', [BKController::class, 'jurusanUpdate'])->name('jurusan.update');
|
||||||
|
Route::delete('/jurusan/{id}', [BKController::class, 'jurusanDestroy'])->name('jurusan.destroy');
|
||||||
|
Route::get('/profil', [BKController::class, 'profil'])->name('profil');
|
||||||
|
Route::put('/profil', [BKController::class, 'updateProfil'])->name('profil.update');
|
||||||
|
Route::put('/profil/password', [BKController::class, 'updatePassword'])->name('profil.password');
|
||||||
|
});
|
||||||
|
|
||||||
require __DIR__.'/auth.php';
|
require __DIR__.'/auth.php';
|
||||||
Loading…
Reference in New Issue