Add 6-digit reset code flow: migration, notification, controller, routes, and forgot-password UI

This commit is contained in:
KakaPatria 2026-05-20 14:38:26 +07:00
parent 24a7387cfb
commit 2905df7309
38 changed files with 2146 additions and 1134 deletions

View File

@ -38,38 +38,90 @@ public function dashboard()
->groupBy('kelompok_asal')
->get();
// Rekomendasi per kelompok
$rekomendasiPerKelompok = Recommendation::selectRaw(
'users.kelompok_asal, COUNT(*) as count'
)
->join('users', 'rekomendasi.user_id', '=', 'users.id')
->groupBy('users.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')
->groupByRaw("JSON_EXTRACT(hasil_rekomendasi, '$[0].jurusan')") ->whereRaw("JSON_EXTRACT(hasil_rekomendasi, '\$[0].jurusan') IS NOT NULL")
->whereRaw("JSON_EXTRACT(hasil_rekomendasi, '\$[0].jurusan') != 'null'") ->orderBy('count', 'desc')
->take(5)
->get();
// Data untuk chart - semua jurusan
// Data untuk chart - semua jurusan (filter out NULL values)
$allMajorsChart = Recommendation::selectRaw("
JSON_EXTRACT(hasil_rekomendasi, '$[0].jurusan') as major_name,
JSON_EXTRACT(hasil_rekomendasi, '\$[0].jurusan') as major_name,
COUNT(*) as count
")
->groupByRaw("JSON_EXTRACT(hasil_rekomendasi, '$[0].jurusan')")
->groupByRaw("JSON_EXTRACT(hasil_rekomendasi, '\$[0].jurusan')")
->whereRaw("JSON_EXTRACT(hasil_rekomendasi, '\$[0].jurusan') IS NOT NULL")
->whereRaw("JSON_EXTRACT(hasil_rekomendasi, '\$[0].jurusan') != 'null'")
->orderBy('count', 'desc')
->get();
// Persiapkan data untuk Chart.js
$chartMajorNames = $allMajorsChart->pluck('major_name')->map(function($name) {
return trim($name, '"');
})->toArray();
$chartMajorCounts = $allMajorsChart->pluck('count')->toArray();
// Persiapkan data untuk Chart.js - aggregate & fix major names
$majorData = [];
foreach ($allMajorsChart as $item) {
$name = trim($item->major_name, '" ');
// Skip empty/null values
if (empty($name) || $name === 'null') {
continue;
}
// Normalize: handle all variants
$normalizedName = $name;
if (stripos($name, 'Teknik Informatika') === 0 || stripos($name, 'Teknologi Informasi') === 0) {
$normalizedName = 'Teknologi Informasi';
}
if (!isset($majorData[$normalizedName])) {
$majorData[$normalizedName] = 0;
}
$majorData[$normalizedName] += (int)$item->count;
}
// Sort by count descending
arsort($majorData);
$chartMajorNames = array_keys($majorData);
$chartMajorCounts = array_values($majorData);
$chartKelompokNames = $kelompokStats->pluck('kelompok_asal')->toArray();
$chartKelompokCounts = $kelompokStats->pluck('count')->toArray();
// Top majors untuk horizontal bar chart
$topMajorsChart = $topMajors->pluck('major_name')->map(function($name) {
return trim($name, '"');
})->toArray();
$topMajorsCounts = $topMajors->pluck('count')->toArray();
// Top majors untuk horizontal bar chart - aggregate & fix
$topMajorData = [];
foreach ($topMajors as $item) {
$name = trim($item->major_name, '" ');
// Skip empty/null values
if (empty($name) || $name === 'null') {
continue;
}
// Normalize: handle all variants
$normalizedName = $name;
if (stripos($name, 'Teknik Informatika') === 0 || stripos($name, 'Teknologi Informasi') === 0) {
$normalizedName = 'Teknologi Informasi';
}
if (!isset($topMajorData[$normalizedName])) {
$topMajorData[$normalizedName] = 0;
}
$topMajorData[$normalizedName] += (int)$item->count;
}
arsort($topMajorData);
$topMajorsChart = array_keys($topMajorData);
$topMajorsCounts = array_values($topMajorData);
return view('admin.dashboard', compact(
'totalSiswa',
@ -85,7 +137,8 @@ public function dashboard()
'chartKelompokNames',
'chartKelompokCounts',
'topMajorsChart',
'topMajorsCounts'
'topMajorsCounts',
'rekomendasiPerKelompok'
));
}
@ -460,4 +513,17 @@ public function updatePassword(Request $request)
return redirect()->route('admin.profil')->with('success', 'Password berhasil diubah!');
}
// ============================================
// 8. LOGOUT SEMUA USER
// ============================================
public function logoutAllUsers()
{
// Logout semua session user
\DB::table('sessions')->truncate();
Auth::logout();
return redirect()->route('login')->with('success', 'Semua user telah berhasil logout!');
}
}

View File

@ -54,8 +54,8 @@ function ($user) use ($request) {
// the application's home authenticated view. If there is an error we can
// redirect them back to where they came from with their error message.
return $status == Password::PASSWORD_RESET
? redirect()->route('login')->with('status', __($status))
? redirect()->route('login')->with('status', '✨ Password berhasil direset! Sekarang Anda bisa login dengan password baru.')
: back()->withInput($request->only('email'))
->withErrors(['email' => __($status)]);
->withErrors(['token' => 'Token reset password telah kadaluarsa atau tidak valid. Silakan minta link reset password baru.']);
}
}

View File

@ -0,0 +1,93 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use App\Models\User;
use Carbon\Carbon;
class PasswordResetWithCodeController extends Controller
{
public function resetWithCode(Request $request)
{
$data = $request->only(['email','token','password','password_confirmation']);
$validator = Validator::make($data, [
'email' => 'required|email',
'token' => 'required|digits:6',
'password' => 'required|string|min:8|confirmed',
]);
if ($validator->fails()) {
return back()->withErrors($validator)->withInput();
}
$email = $data['email'];
$code = $data['token'];
$expiryMinutes = config('auth.passwords.users.expire', 60);
$cutoff = Carbon::now()->subMinutes($expiryMinutes);
$row = DB::table('password_reset_codes')
->where('email', $email)
->where('code', $code)
->where('created_at', '>=', $cutoff)
->first();
if (! $row) {
return back()->withErrors(['token' => 'Token tidak valid atau sudah kadaluarsa.'])->withInput();
}
$user = User::where('email', $email)->first();
if (! $user) {
return back()->withErrors(['email' => 'Akun dengan email ini tidak ditemukan.'])->withInput();
}
$user->password = Hash::make($data['password']);
$user->save();
// remove used codes
DB::table('password_reset_codes')->where('email', $email)->delete();
return redirect()->route('login')->with('status', 'Password berhasil diubah. Silakan login dengan password baru.');
}
/**
* Verify code for an email without changing password (used for two-step flow)
*/
public function verifyCode(Request $request)
{
$data = $request->only(['email','token']);
$validator = \Illuminate\Support\Facades\Validator::make($data, [
'email' => 'required|email',
'token' => 'required|digits:6',
]);
if ($validator->fails()) {
return response()->json(['ok' => false, 'errors' => $validator->errors()->all()], 422);
}
$email = $data['email'];
$code = $data['token'];
$expiryMinutes = config('auth.passwords.users.expire', 60);
$cutoff = Carbon::now()->subMinutes($expiryMinutes);
$row = DB::table('password_reset_codes')
->where('email', $email)
->where('code', $code)
->where('created_at', '>=', $cutoff)
->first();
if (! $row) {
return response()->json(['ok' => false, 'message' => 'Token tidak valid atau sudah kadaluarsa.'], 404);
}
return response()->json(['ok' => true, 'message' => 'Token valid. Silakan masukkan password baru.']);
}
}

View File

@ -70,38 +70,90 @@ public function dashboard()
->groupBy('kelompok_asal')
->get();
// Rekomendasi per kelompok
$rekomendasiPerKelompok = Recommendation::selectRaw(
'users.kelompok_asal, COUNT(*) as count'
)
->join('users', 'rekomendasi.user_id', '=', 'users.id')
->groupBy('users.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')
->groupByRaw("JSON_EXTRACT(hasil_rekomendasi, '$[0].jurusan')") ->whereRaw("JSON_EXTRACT(hasil_rekomendasi, '\$[0].jurusan') IS NOT NULL")
->whereRaw("JSON_EXTRACT(hasil_rekomendasi, '\$[0].jurusan') != 'null'") ->orderBy('count', 'desc')
->take(5)
->get();
// Data untuk chart - semua jurusan
// Data untuk chart - semua jurusan (filter out NULL values)
$allMajorsChart = Recommendation::selectRaw("
JSON_EXTRACT(hasil_rekomendasi, '$[0].jurusan') as major_name,
JSON_EXTRACT(hasil_rekomendasi, '\$[0].jurusan') as major_name,
COUNT(*) as count
")
->groupByRaw("JSON_EXTRACT(hasil_rekomendasi, '$[0].jurusan')")
->groupByRaw("JSON_EXTRACT(hasil_rekomendasi, '\$[0].jurusan')")
->whereRaw("JSON_EXTRACT(hasil_rekomendasi, '\$[0].jurusan') IS NOT NULL")
->whereRaw("JSON_EXTRACT(hasil_rekomendasi, '\$[0].jurusan') != 'null'")
->orderBy('count', 'desc')
->get();
// Persiapkan data untuk Chart.js
$chartMajorNames = $allMajorsChart->pluck('major_name')->map(function($name) {
return trim($name, '"');
})->toArray();
$chartMajorCounts = $allMajorsChart->pluck('count')->toArray();
// Persiapkan data untuk Chart.js - aggregate & fix major names
$majorData = [];
foreach ($allMajorsChart as $item) {
$name = trim($item->major_name, '" ');
// Skip empty/null values
if (empty($name) || $name === 'null') {
continue;
}
// Normalize: handle all variants
$normalizedName = $name;
if (stripos($name, 'Teknik Informatika') === 0 || stripos($name, 'Teknologi Informasi') === 0) {
$normalizedName = 'Teknologi Informasi';
}
if (!isset($majorData[$normalizedName])) {
$majorData[$normalizedName] = 0;
}
$majorData[$normalizedName] += (int)$item->count;
}
// Sort by count descending
arsort($majorData);
$chartMajorNames = array_keys($majorData);
$chartMajorCounts = array_values($majorData);
$chartKelompokNames = $kelompokStats->pluck('kelompok_asal')->toArray();
$chartKelompokCounts = $kelompokStats->pluck('count')->toArray();
// Top majors untuk horizontal bar chart
$topMajorsChart = $topMajors->pluck('major_name')->map(function($name) {
return trim($name, '"');
})->toArray();
$topMajorsCounts = $topMajors->pluck('count')->toArray();
// Top majors untuk horizontal bar chart - aggregate & fix
$topMajorData = [];
foreach ($topMajors as $item) {
$name = trim($item->major_name, '" ');
// Skip empty/null values
if (empty($name) || $name === 'null') {
continue;
}
// Normalize: handle all variants
$normalizedName = $name;
if (stripos($name, 'Teknik Informatika') === 0 || stripos($name, 'Teknologi Informasi') === 0) {
$normalizedName = 'Teknologi Informasi';
}
if (!isset($topMajorData[$normalizedName])) {
$topMajorData[$normalizedName] = 0;
}
$topMajorData[$normalizedName] += (int)$item->count;
}
arsort($topMajorData);
$topMajorsChart = array_keys($topMajorData);
$topMajorsCounts = array_values($topMajorData);
return view('bk.dashboard', compact(
'totalSiswa',
@ -117,7 +169,8 @@ public function dashboard()
'chartKelompokNames',
'chartKelompokCounts',
'topMajorsChart',
'topMajorsCounts'
'topMajorsCounts',
'rekomendasiPerKelompok'
));
}

View File

@ -255,27 +255,25 @@ public function proses(Request $request)
$prior = 1 / $cfgCount;
$logPrior = log(max($prior, $epsilon));
// Weights dan match probabilities dengan defaults (ROC-based: nilai 15.6%, minat 45.6%, pref 25.6%, cita 9%, prestasi 4%)
$weights = $c['weights'] ?? ['nilai' => 0.156, 'minat' => 0.456, 'pref' => 0.256, 'cita_cita' => 0.090, 'prestasi' => 0.040];
// Ensure weights is array
if (!is_array($weights)) {
$weights = ['nilai' => 0.156, 'minat' => 0.456, 'pref' => 0.256, 'cita_cita' => 0.090, 'prestasi' => 0.040];
}
// Use global ROC weights (override any per-jurusan editable weights)
// ROC-based: nilai 15.6%, minat 45.6%, pref 25.6%, cita 9%, prestasi 4%
$globalWeights = ['nilai' => 0.156, 'minat' => 0.456, 'pref' => 0.256, 'cita_cita' => 0.090, 'prestasi' => 0.040];
$weights = $globalWeights;
// Jika prestasi kosong, atribut prestasi tidak dihitung dengan normalisasi ulang
// Jika prestasi kosong, atribut prestasi tidak dihitung dan lakukan normalisasi ulang pada atribut lain
if (!$isPrestasiFilled) {
$weights['prestasi'] = 0.0;
$sumNonPrestasi = ($weights['nilai'] ?? 0) + ($weights['minat'] ?? 0) + ($weights['cita_cita'] ?? 0);
$sumNonPrestasi = ($weights['nilai'] ?? 0) + ($weights['minat'] ?? 0) + ($weights['pref'] ?? 0) + ($weights['cita_cita'] ?? 0);
// Normalize weights dengan safety check
if ($sumNonPrestasi > $epsilon) {
$weights['nilai'] = ($weights['nilai'] ?? 0) / $sumNonPrestasi;
$weights['minat'] = ($weights['minat'] ?? 0) / $sumNonPrestasi;
$weights['pref'] = ($weights['pref'] ?? 0) / $sumNonPrestasi;
$weights['cita_cita'] = ($weights['cita_cita'] ?? 0) / $sumNonPrestasi;
} else {
// Fallback weights jika semua weight adalah 0
$weights = ['nilai' => 0.156, 'minat' => 0.456, 'pref' => 0.256, 'cita_cita' => 0.090, 'prestasi' => 0.040];
// Fallback ke global jika normalisasi gagal
$weights = $globalWeights;
}
}

View File

@ -46,11 +46,20 @@ public function toMail(object $notifiable): MailMessage
'email' => $notifiable->getEmailForPasswordReset(),
], false));
// generate 6-digit numeric code and store it for inline token flow
$code = str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT);
\Illuminate\Support\Facades\DB::table('password_reset_codes')->updateOrInsert(
['email' => $notifiable->getEmailForPasswordReset()],
['code' => $code, 'created_at' => now()]
);
return (new MailMessage)
->subject('🔑 Reset Password - Sistem Pemilihan Jurusan Polije')
->view('emails.reset-password', [
'user' => $notifiable,
'resetUrl' => $resetUrl,
'code' => $code,
'token' => $this->token,
'expiresIn' => config('auth.passwords.users.expire', 60),
]);
}

View File

@ -94,7 +94,7 @@
'users' => [
'provider' => 'users',
'table' => 'password_reset_tokens',
'expire' => 60,
'expire' => 1440, // 24 hours (was 60 minutes)
'throttle' => 60,
],
],

View File

@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('password_reset_codes', function (Blueprint $table) {
$table->id();
$table->string('email')->index();
$table->string('code', 10)->index();
$table->timestamp('created_at')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('password_reset_codes');
}
};

View File

@ -28,7 +28,7 @@ public function run(): void
'cita_cita' => 'Software Developer',
'preferensi_studi' => 'Sains & Teknologi',
'prestasi' => 'Juara 1 Olimpiade Komputer Nasional',
'major_masuk' => 'Teknik Informatika',
'major_masuk' => 'Teknologi Informasi',
'tahun_lulus_polije' => 2027,
'catatan' => 'Alumni 2023 - Rekomendasi akurat',
],

View File

@ -0,0 +1,178 @@
<?php
namespace Database\Seeders;
use App\Models\User;
use App\Models\Recommendation;
use App\Models\ChatHistory;
use App\Models\PolijeMajor;
use Illuminate\Database\Seeder;
use Illuminate\Support\Str;
class Generate30StudentsSeeder extends Seeder
{
/**
* Generate 30 students dengan data, recommendations, dan chat history
* Run: php artisan db:seed --class=Generate30StudentsSeeder
*/
public function run(): void
{
$studentsData = [
// KELOMPOK IPA - 15 siswa
['name' => 'Akmal Fiqri', 'email' => 'akmal.fiqri@school.id', 'kelompok' => 'IPA', 'mtk' => 85, 'fisika' => 82, 'kimia' => 75, 'biologi' => 70, 'minat' => 'Teknologi & Programming'],
['name' => 'Sasha Putri Aulia', 'email' => 'sasha.putri@school.id', 'kelompok' => 'IPA', 'mtk' => 78, 'fisika' => 80, 'kimia' => 85, 'biologi' => 82, 'minat' => 'Kesehatan & Biologi'],
['name' => 'Budi Kusuma', 'email' => 'budi.kusuma@school.id', 'kelompok' => 'IPA', 'mtk' => 82, 'fisika' => 88, 'kimia' => 76, 'biologi' => 68, 'minat' => 'Teknik & Mesin'],
['name' => 'Dina Hartini', 'email' => 'dina.hartini@school.id', 'kelompok' => 'IPA', 'mtk' => 75, 'fisika' => 72, 'kimia' => 88, 'biologi' => 90, 'minat' => 'Farmasi & Kimia'],
['name' => 'Eka Prasetyo', 'email' => 'eka.prasetyo@school.id', 'kelompok' => 'IPA', 'mtk' => 88, 'fisika' => 85, 'kimia' => 72, 'biologi' => 75, 'minat' => 'Pemrograman & Coding'],
['name' => 'Fitra Handoko', 'email' => 'fitra.handoko@school.id', 'kelompok' => 'IPA', 'mtk' => 92, 'fisika' => 90, 'kimia' => 88, 'biologi' => 80, 'minat' => 'Sains & Penelitian'],
['name' => 'Gita Pramesti', 'email' => 'gita.pramesti@school.id', 'kelompok' => 'IPA', 'mtk' => 80, 'fisika' => 78, 'kimia' => 82, 'biologi' => 85, 'minat' => 'Bioteknologi'],
['name' => 'Hendra Wijaya', 'email' => 'hendra.wijaya@school.id', 'kelompok' => 'IPA', 'mtk' => 86, 'fisika' => 84, 'kimia' => 80, 'biologi' => 72, 'minat' => 'Fisika & Teknologi'],
['name' => 'Intan Sari', 'email' => 'intan.sari@school.id', 'kelompok' => 'IPA', 'mtk' => 79, 'fisika' => 81, 'kimia' => 83, 'biologi' => 88, 'minat' => 'Kesehatan'],
['name' => 'Jaka Surya', 'email' => 'jaka.surya@school.id', 'kelompok' => 'IPA', 'mtk' => 83, 'fisika' => 86, 'kimia' => 78, 'biologi' => 70, 'minat' => 'Teknik Elektro'],
['name' => 'Kurniawan Adi', 'email' => 'kurniawan.adi@school.id', 'kelompok' => 'IPA', 'mtk' => 87, 'fisika' => 88, 'kimia' => 85, 'biologi' => 76, 'minat' => 'Teknik Sipil'],
['name' => 'Latifa Zahra', 'email' => 'latifa.zahra@school.id', 'kelompok' => 'IPA', 'mtk' => 81, 'fisika' => 80, 'kimia' => 87, 'biologi' => 84, 'minat' => 'Kedokteran'],
['name' => 'Maulana Rizki', 'email' => 'maulana.rizki@school.id', 'kelompok' => 'IPA', 'mtk' => 84, 'fisika' => 83, 'kimia' => 81, 'biologi' => 73, 'minat' => 'Teknologi Informasi'],
['name' => 'Nisa Rahmawati', 'email' => 'nisa.rahmawati@school.id', 'kelompok' => 'IPA', 'mtk' => 77, 'fisika' => 79, 'kimia' => 84, 'biologi' => 89, 'minat' => 'Perawatan Kesehatan'],
['name' => 'Oki Pratama', 'email' => 'oki.pratama@school.id', 'kelompok' => 'IPA', 'mtk' => 89, 'fisika' => 87, 'kimia' => 84, 'biologi' => 71, 'minat' => 'Energi Terbarukan'],
// KELOMPOK IPS - 15 siswa
['name' => 'Fahmi Rizki', 'email' => 'fahmi.rizki@school.id', 'kelompok' => 'IPS', 'ekonomi' => 88, 'geografi' => 80, 'sosiologi' => 78, 'sejarah' => 75, 'minat' => 'Bisnis & Keuangan'],
['name' => 'Gina Melani', 'email' => 'gina.melani@school.id', 'kelompok' => 'IPS', 'ekonomi' => 85, 'geografi' => 88, 'sosiologi' => 82, 'sejarah' => 80, 'minat' => 'Pariwisata & Budaya'],
['name' => 'Hasan Wijaya', 'email' => 'hasan.wijaya@school.id', 'kelompok' => 'IPS', 'ekonomi' => 82, 'geografi' => 85, 'sosiologi' => 80, 'sejarah' => 78, 'minat' => 'Manajemen & Administrasi'],
['name' => 'Irma Santika', 'email' => 'irma.santika@school.id', 'kelompok' => 'IPS', 'ekonomi' => 75, 'geografi' => 92, 'sosiologi' => 85, 'sejarah' => 88, 'minat' => 'Budaya & Komunikasi'],
['name' => 'Joko Supriyanto', 'email' => 'joko.supriyanto@school.id', 'kelompok' => 'IPS', 'ekonomi' => 80, 'geografi' => 75, 'sosiologi' => 88, 'sejarah' => 82, 'minat' => 'Sosial & Masyarakat'],
['name' => 'Kartika Dewi', 'email' => 'kartika.dewi@school.id', 'kelompok' => 'IPS', 'ekonomi' => 87, 'geografi' => 84, 'sosiologi' => 80, 'sejarah' => 82, 'minat' => 'Akuntansi & Keuangan'],
['name' => 'Lucia Pratiwi', 'email' => 'lucia.pratiwi@school.id', 'kelompok' => 'IPS', 'ekonomi' => 83, 'geografi' => 86, 'sosiologi' => 84, 'sejarah' => 85, 'minat' => 'Ilmu Komunikasi'],
['name' => 'Mahmud Ali', 'email' => 'mahmud.ali@school.id', 'kelompok' => 'IPS', 'ekonomi' => 81, 'geografi' => 78, 'sosiologi' => 85, 'sejarah' => 80, 'minat' => 'Hukum & Keadilan'],
['name' => 'Nadya Salsabila', 'email' => 'nadya.salsabila@school.id', 'kelompok' => 'IPS', 'ekonomi' => 84, 'geografi' => 88, 'sosiologi' => 81, 'sejarah' => 79, 'minat' => 'Perhotelan'],
['name' => 'Oman Sutrisno', 'email' => 'oman.sutrisno@school.id', 'kelompok' => 'IPS', 'ekonomi' => 86, 'geografi' => 82, 'sosiologi' => 79, 'sejarah' => 81, 'minat' => 'Bisnis Internasional'],
['name' => 'Putri Handini', 'email' => 'putri.handini@school.id', 'kelompok' => 'IPS', 'ekonomi' => 80, 'geografi' => 87, 'sosiologi' => 83, 'sejarah' => 84, 'minat' => 'Pendidikan & Pengajaran'],
['name' => 'Qohaf Ramadhan', 'email' => 'qohaf.ramadhan@school.id', 'kelompok' => 'IPS', 'ekonomi' => 82, 'geografi' => 79, 'sosiologi' => 86, 'sejarah' => 83, 'minat' => 'Pemerintahan'],
['name' => 'Rita Kusuma', 'email' => 'rita.kusuma@school.id', 'kelompok' => 'IPS', 'ekonomi' => 85, 'geografi' => 86, 'sosiologi' => 82, 'sejarah' => 81, 'minat' => 'Diplomasi & Hubungan Internasional'],
['name' => 'Satrio Budi', 'email' => 'satrio.budi@school.id', 'kelompok' => 'IPS', 'ekonomi' => 79, 'geografi' => 83, 'sosiologi' => 88, 'sejarah' => 85, 'minat' => 'Jurnalisme'],
['name' => 'Tyas Merika', 'email' => 'tyas.merika@school.id', 'kelompok' => 'IPS', 'ekonomi' => 88, 'geografi' => 85, 'sosiologi' => 80, 'sejarah' => 78, 'minat' => 'Asuransi & Perbankan'],
];
$chatMessages = [
'Halo, apa yang harus saya lakukan untuk memilih jurusan yang tepat?',
'Saya bingung antara dua pilihan jurusan yang berbeda.',
'Bagaimana prospek kerja dari jurusan yang Anda rekomendasikan?',
'Apakah ada beasiswa untuk jurusan ini?',
'Apa saja persyaratan untuk masuk ke jurusan tersebut?',
'Berapa lama program studi ini?',
'Apa saja mata kuliah yang akan saya pelajari?',
'Saya tertarik dengan teknologi, jurusan apa yang cocok?',
'Bagaimana dengan kesempatan magang?',
'Saya ingin bekerja di luar negeri, jurusan apa yang sesuai?',
];
$majoritasNames = PolijeMajor::pluck('nama_jurusan')->toArray();
if (empty($majoritasNames)) {
$this->command->error('❌ Tidak ada jurusan di database. Jalankan seeder jurusan terlebih dahulu!');
return;
}
foreach ($studentsData as $index => $data) {
// Cek email unik
if (User::where('email', $data['email'])->exists()) {
$this->command->warn("⚠️ Email {$data['email']} sudah ada, skip...");
continue;
}
// Create Student User
$user = User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => bcrypt('student123'),
'role' => 'siswa',
'nis' => '20240' . str_pad($index + 1, 3, '0', STR_PAD_LEFT),
'kelompok_asal' => $data['kelompok'],
'email_verified_at' => now(),
]);
$this->command->line("✅ Created student: {$data['name']} ({$data['email']})");
// Create Recommendation
if ($data['kelompok'] === 'IPA') {
$matematika = $data['mtk'];
$fisika = $data['fisika'];
$kimia = $data['kimia'];
$biologi = $data['biologi'];
} else {
$ekonomi = $data['ekonomi'];
$geografi = $data['geografi'];
$sosiologi = $data['sosiologi'];
$sejarah = $data['sejarah'];
}
$hasRecommendation = Recommendation::where('user_id', $user->id)->exists();
if (!$hasRecommendation) {
// Simulate recommendation algorithm scores
$scores = array_fill(0, count($majoritasNames), 0);
// Random weighted scoring
foreach ($scores as $key => $score) {
$scores[$key] = rand(55, 95);
}
// Top 9 recommendations
arsort($scores);
$topScores = array_slice($scores, 0, 9, true);
$hasilRekomendasi = [];
$rank = 1;
foreach ($topScores as $majorIndex => $score) {
$hasilRekomendasi[] = [
'ranking' => $rank,
'jurusan' => $majoritasNames[$majorIndex] ?? 'Teknologi Informasi',
'skor' => round($score),
];
$rank++;
}
$recommendation = Recommendation::create([
'user_id' => $user->id,
'mtk' => $data['mtk'] ?? null,
'fisika' => $data['fisika'] ?? null,
'kimia' => $data['kimia'] ?? null,
'biologi' => $data['biologi'] ?? null,
'ekonomi' => $data['ekonomi'] ?? null,
'geografi' => $data['geografi'] ?? null,
'sosiologi' => $data['sosiologi'] ?? null,
'sejarah' => $data['sejarah'] ?? null,
'minat' => $data['minat'] ?? null,
'preferensi_studi' => $data['pref_studi'] ?? ($data['kelompok'] === 'IPA' ? 'Sains & Teknologi' : 'Sosial & Humaniora'),
'cita_cita' => $data['cita_cita'] ?? ($data['minat'] ?? null),
'prestasi' => $data['prestasi'] ?? 'Aktif',
'hasil_rekomendasi' => json_encode($hasilRekomendasi),
]);
$this->command->info(" 📊 Recommendation created (id: {$recommendation->id})");
}
// Create Chat Histories
$existingChats = ChatHistory::where('user_id', $user->id)->count();
if ($existingChats === 0) {
for ($i = 0; $i < rand(2, 5); $i++) {
$randomMessage = $chatMessages[array_rand($chatMessages)];
ChatHistory::create([
'user_id' => $user->id,
'id_sesi' => null,
'id_rekomendasi' => $recommendation->id ?? null,
'pertanyaan' => $randomMessage,
'jawaban' => 'Terima kasih atas pertanyaannya. Berdasarkan profil akademik dan minat Anda, saya merekomendasikan jurusan yang sesuai dengan kemampuan dan tujuan karir Anda.',
]);
}
$this->command->info(" 💬 Chat histories created (2-5 chats)");
}
}
$this->command->info("\n✨ Seeder selesai! 30 students + recommendations + chat histories berhasil dibuat.\n");
}
}

View File

@ -257,7 +257,7 @@ public function run(): void
Recommendation::create(array_merge($data, [
'hasil_rekomendasi' => [
[
'jurusan' => 'Teknik Informatika',
'jurusan' => 'Teknologi Informasi',
'skor' => rand(70, 95) / 100,
'detail' => [
'nilai' => rand(60, 95) / 100,
@ -274,7 +274,7 @@ public function run(): void
// Data chat history yang natural dan tidak template
$chatData = [
// Chat tentang rekomendasi
['user_id' => 2, 'id_sesi' => 'session_1', 'pertanyaan' => 'Jadi menurut sistem saya cocok ambil jurusan apa ya?', 'jawaban' => 'Berdasarkan hasil analisis, untuk profile Anda yang nilai MTK-Fisika bagus dan minat coding kuat, Teknik Informatika adalah pilihan yang selaras. Anda bisa fokus cybersecurity atau development.'],
['user_id' => 2, 'id_sesi' => 'session_1', 'pertanyaan' => 'Jadi menurut sistem saya cocok ambil jurusan apa ya?', 'jawaban' => 'Berdasarkan hasil analisis, untuk profile Anda yang nilai MTK-Fisika bagus dan minat coding kuat, Teknologi Informasi adalah pilihan yang selaras. Anda bisa fokus cybersecurity atau development.'],
['user_id' => 2, 'id_sesi' => 'session_1', 'pertanyaan' => 'Cybersecurity lebih amankan karir atau development?', 'jawaban' => 'Dua-duanya prospek bagus. Cybersecurity ada demand dari government dan finance yang stabil. Development skills lebih flexible bisa startup atau big tech. Lihat passion Anda lebih ke yang mana.'],
['user_id' => 3, 'id_sesi' => 'session_2', 'pertanyaan' => 'Saya pengen jurusan yang bisa langsung applicable untuk robotika', 'jawaban' => 'Untuk robotika, Teknik Elektro atau Teknik Mesin sangat applicable. Dengan score fisika 90 Anda, dua jurusan ini cocok. Teknik Elektro kalau fokus control system, Teknik Mesin kalau fokus mechanical design.'],
['user_id' => 3, 'id_sesi' => 'session_2', 'pertanyaan' => 'Lebih pilih mana sih?', 'jawaban' => 'Lihat passion Anda lebih ke elektrik atau mekanik? Kalau suka coding dan control systems, Teknik Elektro. Kalau suka design fisik dan assemble-dismantle, Teknik Mesin. Keduanya relevant untuk robotika.'],
@ -310,7 +310,7 @@ public function run(): void
['user_id' => 11, 'id_sesi' => 'session_10', 'pertanyaan' => 'Ada lab atau praktik langsung nggak?', 'jawaban' => 'Ada. Biasanya ada lab communicative, media production, bahkan ada broadcast studio. Praktik langsung dengan project nyata untuk client atau media partner. Itu yang membuat komunikasi menarik.'],
// More natural chats
['user_id' => 2, 'id_sesi' => 'session_11', 'pertanyaan' => 'Kenalin dong senior-senior di Teknik Informatika', 'jawaban' => 'Udah ada program mentoring di Polije. Senior bisa di-connect melalui alumni network atau student organization. Mereka helpful banget share experience dan tips lolos interview di tech company.'],
['user_id' => 2, 'id_sesi' => 'session_11', 'pertanyaan' => 'Kenalin dong senior-senior di Teknologi Informasi', 'jawaban' => 'Udah ada program mentoring di Polije. Senior bisa di-connect melalui alumni network atau student organization. Mereka helpful banget share experience dan tips lolos interview di tech company.'],
['user_id' => 3, 'id_sesi' => 'session_12', 'pertanyaan' => 'Lab di Teknik Elektro lengkap nggak untuk robotika?', 'jawaban' => 'Lumayan lengkap. Ada microcontroller lab, digital logic lab. Untuk advanced robotics, bisa kolaborasi dengan Teknik Mesin juga. Ada makerspace bersama yang membantu student projects.'],
['user_id' => 4, 'id_sesi' => 'session_13', 'pertanyaan' => 'Kesehatan di Polije fokus apa ya?', 'jawaban' => 'Ada program Ilmu Gizi, Teknologi Laboratorium Medis, dan Kesehatan Masyarakat. Fokus ke applied health, tidak pure medical science. Cocok untuk Anda yang peduli community health.'],
['user_id' => 5, 'id_sesi' => 'session_14', 'pertanyaan' => 'Mahasiswa agribusiness banyak nggak?', 'jawaban' => 'Cukup banyak, sekitar 100 per tahun. Intake lumayan tinggi karena demand industri. Networking sama peers bisa bagus untuk future business partnership.'],

207
database_test.php Normal file
View File

@ -0,0 +1,207 @@
<?php
/**
* DATABASE INTEGRITY TEST SCRIPT
* Untuk memverifikasi data sebelum sidang
*
* Jalankan dari terminal:
* php artisan tinker < database_test.php
* atau copy-paste di tinker session
*/
// ============================================
// TEST 1: CHECK NULL VALUES IN RECOMMENDATIONS
// ============================================
echo "\n============================================\n";
echo "✅ TEST 1: CHECK NULL VALUES IN RECOMMENDATIONS\n";
echo "============================================\n";
$nullCount = \App\Models\Recommendation::whereRaw("JSON_EXTRACT(hasil_rekomendasi, '$[0].jurusan') IS NULL")
->orWhereRaw("JSON_EXTRACT(hasil_rekomendasi, '$[0].jurusan') = 'null'")
->count();
if ($nullCount === 0) {
echo "✅ PASS: Tidak ada NULL values\n";
echo " Total recommendations: " . \App\Models\Recommendation::count() . "\n";
} else {
echo "❌ FAIL: Ditemukan $nullCount NULL values!\n";
}
// ============================================
// TEST 2: CHECK MAJOR NAMES CONSISTENCY
// ============================================
echo "\n============================================\n";
echo "✅ TEST 2: CHECK MAJOR NAMES CONSISTENCY\n";
echo "============================================\n";
$majors = \App\Models\PolijeMajor::pluck('nama_jurusan')->unique();
echo "Jurusan yang ada di database:\n";
foreach ($majors as $major) {
echo " - $major\n";
}
// Check for wrong naming
$wrongNaming = \App\Models\Recommendation::selectRaw("JSON_EXTRACT(hasil_rekomendasi, '$[0].jurusan') as major_name")
->distinct()
->get();
echo "\nMajors dalam rekomendasi:\n";
$hasWrongName = false;
foreach ($wrongNaming as $item) {
$name = trim($item->major_name, '\" ');
if (strpos($name, 'Teknik Informatika') !== false) {
echo " ⚠️ $name (SHOULD BE: Teknologi Informasi)\n";
$hasWrongName = true;
} elseif (!empty($name) && $name !== 'null') {
echo "$name\n";
}
}
if ($hasWrongName) {
echo "\n❌ FAIL: Ada major name yang salah! (Teknik Informatika should be Teknologi Informasi)\n";
} else {
echo "\n✅ PASS: Semua major names konsisten\n";
}
// ============================================
// TEST 3: CHECK WEIGHTS STANDARDIZATION
// ============================================
echo "\n============================================\n";
echo "✅ TEST 3: CHECK WEIGHTS STANDARDIZATION\n";
echo "============================================\n";
$config = config('polije.jurusan');
$expectedWeights = [
'nilai' => 0.156,
'minat' => 0.456,
'pref' => 0.256,
'cita_cita' => 0.090,
'prestasi' => 0.040
];
$weightsValid = true;
echo "Expected weights:\n";
foreach ($expectedWeights as $key => $weight) {
echo " $key: $weight\n";
}
echo "\nActual weights in config:\n";
foreach ($config as $jurusan => $data) {
$weights = $data['weights'];
$total = array_sum($weights);
$isValid = true;
foreach ($expectedWeights as $key => $expectedWeight) {
if ($weights[$key] != $expectedWeight) {
$isValid = false;
break;
}
}
if ($isValid && abs($total - 1.0) < 0.001) {
echo "$jurusan (total: $total)\n";
} else {
echo "$jurusan - MISMATCH!\n";
echo " Weights: " . json_encode($weights) . "\n";
$weightsValid = false;
}
}
if ($weightsValid) {
echo "\n✅ PASS: Semua jurusan memiliki weights yang sama dan valid\n";
} else {
echo "\n❌ FAIL: Ada weights yang tidak sesuai!\n";
}
// ============================================
// TEST 4: CHECK KEYWORDS BALANCE
// ============================================
echo "\n============================================\n";
echo "✅ TEST 4: CHECK KEYWORDS BALANCE\n";
echo "============================================\n";
$keywords = config('polije.keywords');
echo "Minat Keywords Balance:\n";
$minatBalance = true;
foreach ($keywords['minat'] as $category => $kwords) {
$count = count($kwords);
if ($count >= 24 && $count <= 28) {
echo "$category: $count keywords\n";
} else {
echo " ⚠️ $category: $count keywords (ideal: 26)\n";
$minatBalance = false;
}
}
echo "\nCita-Cita Keywords Balance:\n";
$citaBalance = true;
foreach ($keywords['cita_cita'] as $category => $kwords) {
$count = count($kwords);
if ($count >= 24 && $count <= 30) {
echo "$category: $count keywords\n";
} else {
echo " ⚠️ $category: $count keywords (ideal: 24-30)\n";
$citaBalance = false;
}
}
if ($minatBalance && $citaBalance) {
echo "\n✅ PASS: Keywords balance OK\n";
} else {
echo "\n⚠️ WARNING: Ada keywords yang tidak balanced\n";
}
// ============================================
// TEST 5: CHECK TOTAL RECORDS
// ============================================
echo "\n============================================\n";
echo "✅ TEST 5: TOTAL RECORDS CHECK\n";
echo "============================================\n";
$stats = [
'Total Users' => \App\Models\User::count(),
'Total Siswa' => \App\Models\User::where('role', 'siswa')->count(),
'Total BK' => \App\Models\User::where('role', 'bk')->count(),
'Total Admin' => \App\Models\User::where('role', 'admin')->count(),
'Total Recommendations' => \App\Models\Recommendation::count(),
'Total Chat History' => \App\Models\ChatHistory::count(),
'Total Jurusan' => \App\Models\PolijeMajor::count(),
];
foreach ($stats as $label => $count) {
echo " $label: $count\n";
}
// ============================================
// TEST 6: SAMPLE RECOMMENDATION CHECK
// ============================================
echo "\n============================================\n";
echo "✅ TEST 6: SAMPLE RECOMMENDATION CHECK\n";
echo "============================================\n";
$sample = \App\Models\Recommendation::with('user')->first();
if ($sample) {
echo "Sample Recommendation:\n";
echo " User: " . $sample->user->name . "\n";
echo " Input Data:\n";
echo " - Nilai Rata-rata: " . ($sample->input_data['nilai_rata_rata'] ?? 'N/A') . "\n";
echo " - Minat (Raw): " . ($sample->input_data['minat_raw'] ?? 'N/A') . "\n";
echo " - Minat (Mapped): " . ($sample->input_data['minat_mapped'] ?? 'N/A') . "\n";
echo " - Cita-cita (Raw): " . ($sample->input_data['cita_cita_raw'] ?? 'N/A') . "\n";
echo " - Cita-cita (Mapped): " . ($sample->input_data['cita_cita_mapped'] ?? 'N/A') . "\n";
echo " Top Recommendation:\n";
$topRec = $sample->hasil_rekomendasi[0];
echo " - Jurusan: " . $topRec['jurusan'] . "\n";
echo " - Percentage: " . $topRec['persentase'] . "%\n";
} else {
echo "❌ Tidak ada rekomendasi ditemukan\n";
}
// ============================================
// SUMMARY
// ============================================
echo "\n============================================\n";
echo "✅ TEST COMPLETE\n";
echo "============================================\n";
echo "\nJika semua test PASS ✅, sistem siap untuk sidang!\n";
echo "Jika ada yang FAIL ❌, silahkan perbaiki terlebih dahulu.\n\n";

File diff suppressed because one or more lines are too long

View File

@ -55,7 +55,7 @@
<td class="px-6 py-4 text-center gap-2 flex justify-center">
<a href="{{ route('admin.alumni.show', $a->id) }}" class="text-blue-600 hover:text-blue-800 font-semibold text-sm">👁 Lihat</a>
<a href="{{ route('admin.alumni.edit', $a->id) }}" class="text-yellow-600 hover:text-yellow-800 font-semibold text-sm"> Edit</a>
<form action="{{ route('admin.alumni.destroy', $a->id) }}" method="POST" class="inline" onsubmit="return confirm('Yakin hapus?')">
<form action="{{ route('admin.alumni.destroy', $a->id) }}" method="POST" class="inline swal-confirm" data-confirm-message="Yakin hapus?">
@csrf @method('DELETE')
<button type="submit" class="text-red-600 hover:text-red-800 font-semibold text-sm">🗑 Hapus</button>
</form>

View File

@ -83,7 +83,7 @@
<a href="{{ route('admin.alumni.edit', $alumni->id) }}" class="px-6 py-2 rounded-lg font-bold bg-yellow-400 text-maroon hover:bg-yellow-300 transition">
Edit
</a>
<form action="{{ route('admin.alumni.destroy', $alumni->id) }}" method="POST" class="inline" onsubmit="return confirm('Yakin hapus data alumni ini?')">
<form action="{{ route('admin.alumni.destroy', $alumni->id) }}" method="POST" class="inline swal-confirm" data-confirm-message="Yakin hapus data alumni ini?">
@csrf @method('DELETE')
<button type="submit" class="px-6 py-2 rounded-lg font-bold bg-red-500 text-white hover:bg-red-600 transition">
🗑 Hapus

View File

@ -3,6 +3,13 @@
@section('title', 'Dashboard')
@section('content')
<!-- Logout All Users Button (hidden for safety during sidang) -->
{{-- <div class="mb-6">
<button onclick="confirmLogoutAllUsers()" class="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition flex items-center gap-2 font-semibold">
🚪 Logout Semua User
</button>
</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-maroon">
@ -42,13 +49,13 @@
</div>
</div>
<!-- Kelompok Distribution -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<!-- Kelompok Chart -->
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-maroon">
<h3 class="text-lg font-bold text-maroon mb-4">📊 Siswa per Kelompok</h3>
<div style="position: relative; height: 250px;">
<canvas id="chartKelompokPie"></canvas>
<!-- Rekomendasi & Top Majors -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<!-- Rekomendasi per Kelompok -->
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-maroon">
<h3 class="text-lg font-bold text-maroon mb-4">📊 Rekomendasi per Kelompok</h3>
<div style="position: relative; height: 250px;">
<canvas id="chartRekomendasiKelompok"></canvas>
</div>
</div>
@ -67,7 +74,7 @@
@if($recentStudents->isNotEmpty())
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead class="border-b-2 border-maroon">
<thead class="border-b-2 border-purple-500">
<tr>
<th class="text-left px-4 py-2 font-bold text-maroon">Nama</th>
<th class="text-center px-4 py-2 font-bold text-maroon">NIS</th>
@ -86,7 +93,7 @@
</span>
</td>
<td class="px-4 py-2 text-center">
<a href="{{ route('admin.student.detail', $student->id) }}" class="text-blue-600 hover:text-blue-800 font-semibold text-xs">👁 Lihat</a>
<a href="{{ route('admin.student.detail', $student->id) }}" class="text-purple-600 hover:text-purple-800 font-semibold text-xs">👁 Lihat</a>
</td>
</tr>
@endforeach
@ -207,17 +214,18 @@
}
});
// Chart 3: Kelompok Pie Chart
const chartKelompokPieCtx = document.getElementById('chartKelompokPie').getContext('2d');
const chartKelompokPie = new Chart(chartKelompokPieCtx, {
type: 'pie',
// Chart 3: Rekomendasi per Kelompok Bar Chart
const chartRekomendasiKelompokCtx = document.getElementById('chartRekomendasiKelompok').getContext('2d');
const chartRekomendasiKelompok = new Chart(chartRekomendasiKelompokCtx, {
type: 'bar',
data: {
labels: @json($chartKelompokNames),
labels: @json($rekomendasiPerKelompok->pluck('kelompok_asal')->toArray()),
datasets: [{
data: @json($chartKelompokCounts),
label: 'Jumlah Rekomendasi',
data: @json($rekomendasiPerKelompok->pluck('count')->toArray()),
backgroundColor: ['#0369A1', '#D97706'],
borderColor: '#fff',
borderWidth: 2
borderColor: ['#0369A1', '#D97706'],
borderWidth: 1
}]
},
options: {
@ -225,10 +233,17 @@
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
display: true,
labels: {
font: { size: 11 },
padding: 10
font: { size: 11 }
}
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
font: { size: 10 }
}
}
}
@ -272,4 +287,34 @@
}
});
</script>
<!-- Logout All Users Confirmation Script -->
<script>
function confirmLogoutAllUsers() {
// First confirmation
if (!confirm('⚠️ WARNING! Ini akan logout SEMUA user (Siswa, BK, Admin)!\\n\\nYakin ingin melanjutkan?')) {
return;
}
// Second confirmation
if (!confirm('🔔 Konfirmasi sekali lagi!\\n\\nIni tidak bisa dibatalkan. Lanjutkan?')) {
return;
}
// Submit form via POST
const form = document.createElement('form');
form.method = 'POST';
form.action = '{{ route("admin.logout-all-users") }}';
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
const csrfInput = document.createElement('input');
csrfInput.type = 'hidden';
csrfInput.name = '_token';
csrfInput.value = csrfToken;
form.appendChild(csrfInput);
document.body.appendChild(form);
form.submit();
}
</script>
@endsection

View File

@ -36,7 +36,7 @@
<a href="{{ route('admin.guru-bk.edit', $guru->id) }}" class="px-2 py-1 bg-blue-100 text-blue-700 rounded text-xs font-semibold hover:bg-blue-200 transition inline-block">
Edit
</a>
<form method="POST" action="{{ route('admin.guru-bk.destroy', $guru->id) }}" style="display:inline;" onsubmit="return confirm('Hapus akun guru BK ini?')">
<form method="POST" action="{{ route('admin.guru-bk.destroy', $guru->id) }}" style="display:inline;" class="swal-confirm" data-confirm-message="Hapus akun guru BK ini?">
@csrf
@method('DELETE')
<button type="submit" class="px-2 py-1 bg-red-100 text-red-700 rounded text-xs font-semibold hover:bg-red-200 transition">

View File

@ -82,55 +82,35 @@
</div>
</div>
<!-- Bobot Mata Pelajaran -->
<!-- ROC Weights Info (Fixed) -->
<div class="bg-white rounded-lg shadow p-6">
<h3 class="text-lg font-bold text-maroon 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>
<h3 class="text-lg font-bold text-maroon mb-4">⚖️ Bobot Kriteria Penilaian (ROC-Validated)</h3>
<p class="text-xs text-gray-600 mb-4">Semua jurusan menggunakan bobot yang sama dan telah divalidasi menggunakan ROC curve analysis:</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_mapel[ipa][mtk]" value="{{ old('bobot_mapel.ipa.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-maroon text-sm">
</div>
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">Fisika</label>
<input type="number" name="bobot_mapel[ipa][fisika]" value="{{ old('bobot_mapel.ipa.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-maroon text-sm">
</div>
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">Kimia</label>
<input type="number" name="bobot_mapel[ipa][kimia]" value="{{ old('bobot_mapel.ipa.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-maroon text-sm">
</div>
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">Biologi</label>
<input type="number" name="bobot_mapel[ipa][biologi]" value="{{ old('bobot_mapel.ipa.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-maroon text-sm">
</div>
</div>
<div class="grid grid-cols-2 md:grid-cols-5 gap-3">
<div class="p-3 bg-green-50 rounded-lg text-center border-l-4 border-green-400">
<p class="text-xs font-bold text-green-800">💡 Minat</p>
<p class="text-lg font-bold text-green-700">45.6%</p>
</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_mapel[ips][ekonomi]" value="{{ old('bobot_mapel.ips.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-maroon text-sm">
</div>
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">Geografi</label>
<input type="number" name="bobot_mapel[ips][geografi]" value="{{ old('bobot_mapel.ips.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-maroon text-sm">
</div>
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">Sosiologi</label>
<input type="number" name="bobot_mapel[ips][sosiologi]" value="{{ old('bobot_mapel.ips.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-maroon text-sm">
</div>
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">Sejarah</label>
<input type="number" name="bobot_mapel[ips][sejarah]" value="{{ old('bobot_mapel.ips.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-maroon text-sm">
</div>
</div>
<div class="p-3 bg-yellow-50 rounded-lg text-center border-l-4 border-yellow-400">
<p class="text-xs font-bold text-yellow-800">🎯 Preferensi</p>
<p class="text-lg font-bold text-yellow-700">25.6%</p>
</div>
<div class="p-3 bg-blue-50 rounded-lg text-center border-l-4 border-blue-400">
<p class="text-xs font-bold text-blue-800">📝 Nilai</p>
<p class="text-lg font-bold text-blue-700">15.6%</p>
</div>
<div class="p-3 bg-red-50 rounded-lg text-center border-l-4 border-red-400">
<p class="text-xs font-bold text-red-800">💼 Cita-cita</p>
<p class="text-lg font-bold text-red-700">9.0%</p>
</div>
<div class="p-3 bg-purple-50 rounded-lg text-center border-l-4 border-purple-400">
<p class="text-xs font-bold text-purple-800">🏆 Prestasi</p>
<p class="text-lg font-bold text-purple-700">4.0%</p>
</div>
</div>
<p class="text-xs text-gray-600 mt-4"> Total: 100% | Bobot ini tidak dapat diubah per jurusan untuk konsistensi sistem</p>
</div>
<div class="flex gap-4">

View File

@ -90,60 +90,40 @@
</div>
</div>
<!-- Bobot Mata Pelajaran -->
<!-- ROC Weights Info (Fixed) -->
@php
$bobot = $jurusan->bobot_mapel ?? [];
$bobotIpa = data_get($bobot, 'ipa', $bobot);
$bobotIps = data_get($bobot, 'ips', $bobot);
@endphp
<div class="bg-white rounded-lg shadow p-6">
<h3 class="text-lg font-bold text-maroon 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>
<h3 class="text-lg font-bold text-maroon mb-4">⚖️ Bobot Kriteria Penilaian (ROC-Validated)</h3>
<p class="text-xs text-gray-600 mb-4">Semua jurusan menggunakan bobot yang sama dan telah divalidasi menggunakan ROC curve analysis:</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_mapel[ipa][mtk]" value="{{ old('bobot_mapel.ipa.mtk', data_get($bobotIpa, 'mtk', data_get($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-maroon text-sm">
</div>
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">Fisika</label>
<input type="number" name="bobot_mapel[ipa][fisika]" value="{{ old('bobot_mapel.ipa.fisika', data_get($bobotIpa, 'fisika', data_get($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-maroon text-sm">
</div>
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">Kimia</label>
<input type="number" name="bobot_mapel[ipa][kimia]" value="{{ old('bobot_mapel.ipa.kimia', data_get($bobotIpa, 'kimia', data_get($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-maroon text-sm">
</div>
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">Biologi</label>
<input type="number" name="bobot_mapel[ipa][biologi]" value="{{ old('bobot_mapel.ipa.biologi', data_get($bobotIpa, 'biologi', data_get($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-maroon text-sm">
</div>
</div>
<div class="grid grid-cols-2 md:grid-cols-5 gap-3">
<div class="p-3 bg-green-50 rounded-lg text-center border-l-4 border-green-400">
<p class="text-xs font-bold text-green-800">💡 Minat</p>
<p class="text-lg font-bold text-green-700">45.6%</p>
</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_mapel[ips][ekonomi]" value="{{ old('bobot_mapel.ips.ekonomi', data_get($bobotIps, 'ekonomi', data_get($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-maroon text-sm">
</div>
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">Geografi</label>
<input type="number" name="bobot_mapel[ips][geografi]" value="{{ old('bobot_mapel.ips.geografi', data_get($bobotIps, 'geografi', data_get($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-maroon text-sm">
</div>
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">Sosiologi</label>
<input type="number" name="bobot_mapel[ips][sosiologi]" value="{{ old('bobot_mapel.ips.sosiologi', data_get($bobotIps, 'sosiologi', data_get($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-maroon text-sm">
</div>
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">Sejarah</label>
<input type="number" name="bobot_mapel[ips][sejarah]" value="{{ old('bobot_mapel.ips.sejarah', data_get($bobotIps, 'sejarah', data_get($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-maroon text-sm">
</div>
</div>
<div class="p-3 bg-yellow-50 rounded-lg text-center border-l-4 border-yellow-400">
<p class="text-xs font-bold text-yellow-800">🎯 Preferensi</p>
<p class="text-lg font-bold text-yellow-700">25.6%</p>
</div>
<div class="p-3 bg-blue-50 rounded-lg text-center border-l-4 border-blue-400">
<p class="text-xs font-bold text-blue-800">📝 Nilai</p>
<p class="text-lg font-bold text-blue-700">15.6%</p>
</div>
<div class="p-3 bg-red-50 rounded-lg text-center border-l-4 border-red-400">
<p class="text-xs font-bold text-red-800">💼 Cita-cita</p>
<p class="text-lg font-bold text-red-700">9.0%</p>
</div>
<div class="p-3 bg-purple-50 rounded-lg text-center border-l-4 border-purple-400">
<p class="text-xs font-bold text-purple-800">🏆 Prestasi</p>
<p class="text-lg font-bold text-purple-700">4.0%</p>
</div>
</div>
<p class="text-xs text-gray-600 mt-4"> Total: 100% | Bobot ini tidak dapat diubah per jurusan untuk konsistensi sistem</p>
</div>
<div class="flex gap-4">

View File

@ -70,7 +70,7 @@
<a href="{{ route('admin.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('admin.jurusan.destroy', $jurusan->id) }}" method="POST" onsubmit="return confirm('Yakin ingin menghapus jurusan {{ $jurusan->nama_jurusan }}?')">
<form action="{{ route('admin.jurusan.destroy', $jurusan->id) }}" method="POST" class="swal-confirm" data-confirm-message="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">

View File

@ -7,14 +7,14 @@
<title>@yield('title', 'Admin Panel') - SPK Jurusan Polije</title>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<style>
.gradient-maroon { background: linear-gradient(135deg, #6B7280 0%, #8B95A5 100%); }
.text-maroon { color: #6B7280; }
.border-maroon { border-color: #6B7280; }
.bg-cream { background-color: #F8FAFC; }
.bg-maroon { background-color: #6B7280; }
.hover\:bg-maroon:hover { background-color: #8B95A5; }
.gradient-maroon { background: linear-gradient(135deg, #7c3aed 0%, #6366f1 100%); }
.text-maroon { color: #7c3aed; }
.border-maroon { border-color: #7c3aed; }
.bg-cream { background-color: #f8fafc; }
.bg-maroon { background-color: #7c3aed; }
.hover\:bg-maroon:hover { background-color: #6366f1; }
.stat-card { transition: all 0.3s ease; }
.stat-card:hover { transform: translateY(-5px); box-shadow: 0 5px 15px rgba(107, 114, 128, 0.1); }
.stat-card:hover { transform: translateY(-5px); box-shadow: 0 5px 15px rgba(124, 58, 237, 0.1); }
/* Sidebar */
.sidebar-dark {
@ -27,14 +27,14 @@
color: #cbd5e1;
}
.sidebar-link:hover {
background: rgba(107, 114, 128, 0.12);
background: rgba(124, 58, 237, 0.12);
color: #ffffff;
border-left-color: rgba(107, 114, 128, 0.5);
border-left-color: rgba(124, 58, 237, 0.5);
}
.sidebar-link.active {
background: linear-gradient(90deg, rgba(107,114,128,0.2) 0%, rgba(107,114,128,0.03) 100%);
color: #b0b9c8 !important;
border-left-color: #b0b9c8;
background: linear-gradient(90deg, rgba(124, 58, 237, 0.2) 0%, rgba(124, 58, 237, 0.03) 100%);
color: #c4b5fd !important;
border-left-color: #c4b5fd;
}
.sidebar-link .sidebar-icon {
display: inline-flex;
@ -49,11 +49,11 @@
transition: all 0.25s ease;
}
.sidebar-link:hover .sidebar-icon {
background: rgba(107, 114, 128, 0.25);
background: rgba(124, 58, 237, 0.25);
transform: scale(1.05);
}
.sidebar-link.active .sidebar-icon {
background: rgba(107, 114, 128, 0.15);
background: rgba(124, 58, 237, 0.15);
}
.sidebar-section-label {
font-size: 10px;
@ -73,13 +73,13 @@
.sidebar-brand-icon {
width: 40px;
height: 40px;
background: linear-gradient(135deg, #6B7280 0%, #8B95A5 100%);
background: linear-gradient(135deg, #7c3aed 0%, #6366f1 100%);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
box-shadow: 0 4px 12px rgba(107, 114, 128, 0.3);
box-shadow: 0 4px 12px rgba(124, 58, 237, 0.3);
}
.sidebar-footer {
border-top: 1px solid rgba(255,255,255,0.06);
@ -120,7 +120,7 @@
<a href="{{ route('admin.profil') }}" class="block px-4 py-3 hover:bg-gray-50 text-xs sm:text-sm font-semibold border-b">
👤 Profil Admin
</a>
<form method="POST" action="{{ route('logout') }}">
<form method="POST" action="{{ route('logout') }}" class="confirm-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
@ -279,6 +279,50 @@
mobileOverlay.addEventListener('click', () => mobileSidebar.classList.add('hidden'));
closeMobileMenu.addEventListener('click', () => mobileSidebar.classList.add('hidden'));
</script>
<!-- SweetAlert2 (admin) -->
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('form.confirm-logout').forEach(function(form) {
form.addEventListener('submit', function (e) {
e.preventDefault();
Swal.fire({
title: 'Konfirmasi Logout',
text: 'Yakin ingin logout dari akun ini?',
icon: 'question',
showCancelButton: true,
confirmButtonText: 'Ya, Logout',
cancelButtonText: 'Batal',
reverseButtons: true
}).then(function(result) {
if (result.isConfirmed) {
form.submit();
}
});
});
// Generic SweetAlert2 confirmation for destructive actions in admin
document.querySelectorAll('form.swal-confirm').forEach(function(form) {
form.addEventListener('submit', function(e) {
e.preventDefault();
const msg = form.getAttribute('data-confirm-message') || 'Yakin melanjutkan aksi ini?';
Swal.fire({
title: 'Konfirmasi',
text: msg,
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Ya',
cancelButtonText: 'Batal',
reverseButtons: true
}).then(function(result) {
if (result.isConfirmed) {
form.submit();
}
});
});
});
});
});
</script>
@yield('scripts')
</body>
</html>

View File

@ -52,6 +52,64 @@
</div>
</div>
</div>
<!-- Show token input panel immediately after successful send -->
@php
$prefillCode = '';
$emailForCode = old('email') ?? request()->query('email') ?? request()->input('email');
if (!$emailForCode) {
// try to retrieve email from old input stored in session
$old = session()->get('_old_input', []);
if (!empty($old['email'])) $emailForCode = $old['email'];
}
if ($emailForCode) {
try {
$expiryMinutes = config('auth.passwords.users.expire', 60);
$cutoff = \Carbon\Carbon::now()->subMinutes($expiryMinutes);
$row = \Illuminate\Support\Facades\DB::table('password_reset_codes')
->where('email', $emailForCode)
->where('created_at', '>=', $cutoff)
->first();
if ($row) $prefillCode = $row->code;
} catch (\Throwable $e) {
$prefillCode = '';
}
}
@endphp
<div id="postSendTokenPanel" class="mb-6 bg-white rounded-xl shadow-lg p-6 border-t-4 border-gray-200">
<h4 class="text-sm font-bold text-gray-700 mb-2">Sudah menerima kode 6-digit?</h4>
<p class="text-xs text-gray-500 mb-3">Jika sudah, masukkan kode di bawah agar Anda bisa langsung mengganti password di halaman ini.</p>
<form method="POST" action="{{ route('password.reset.with_code') }}" onsubmit="return handleTokenSubmit(event)">
@csrf
<div class="grid grid-cols-1 gap-3">
<div>
<label class="block text-xs font-semibold text-gray-700 mb-1">Email</label>
<input type="email" name="email" id="post_token_email" value="{{ old('email') ?? request()->query('email', '') }}" required class="w-full px-3 py-2 border rounded-md text-sm focus:ring-2 focus:ring-purple-200 focus:border-purple-500" />
</div>
<div>
<label class="block text-xs font-semibold text-gray-700 mb-1">Kode 6-digit</label>
<input type="text" name="token" id="post_token_input" inputmode="numeric" pattern="\d{6}" placeholder="123456" value="{{ $prefillCode }}" required class="w-full px-3 py-2 border rounded-md text-sm focus:ring-2 focus:ring-purple-200 focus:border-purple-500" />
</div>
<div id="passwordFields" class="hidden">
<div>
<label class="block text-xs font-semibold text-gray-700 mb-1">Password Baru</label>
<input type="password" name="password" id="post_token_password" class="w-full px-3 py-2 border rounded-md text-sm focus:ring-2 focus:ring-purple-200 focus:border-purple-500" />
</div>
<div>
<label class="block text-xs font-semibold text-gray-700 mb-1">Konfirmasi Password</label>
<input type="password" name="password_confirmation" id="post_token_password_confirmation" class="w-full px-3 py-2 border rounded-md text-sm focus:ring-2 focus:ring-purple-200 focus:border-purple-500" />
</div>
</div>
</div>
<div class="mt-4 flex gap-2">
<button type="button" id="verifyCodeBtn" onclick="verifyCode()" class="inline-flex items-center gap-2 bg-gray-200 text-gray-800 text-sm px-4 py-2 rounded-md font-semibold">🔎 Verifikasi Kode</button>
<button type="submit" id="postTokenSubmitBtn" disabled class="inline-flex items-center gap-2 bg-gradient-to-r from-purple-600 to-indigo-600 text-white text-sm px-4 py-2 rounded-md font-semibold opacity-60 cursor-not-allowed">🔒 Reset dengan Kode</button>
</div>
</form>
</div>
@endif
<!-- Error Messages -->
@ -118,6 +176,8 @@ class="w-full bg-gradient-to-r from-purple-600 to-indigo-600 text-white font-bol
<div class="text-center mt-6 text-xs text-gray-500">
<p>💡 Jika email tidak masuk, cek folder spam kamu.</p>
</div>
<!-- (Token panel removed) -->
</div>
</div>
@ -134,6 +194,9 @@ function handleForgotSubmit(event) {
return false;
}
// Save email locally so we can prefill the post-send token panel
try { localStorage.setItem('password_reset_email', email); } catch(e){}
// Disable button dan tampilkan loading state
submitBtn.disabled = true;
submitBtn.textContent = '⏳ Sedang Mengirim...';
@ -142,5 +205,158 @@ function handleForgotSubmit(event) {
return true;
}
</script>
<script>
function handleTokenSubmit(event) {
const email = document.getElementById('post_token_email').value.trim();
const token = document.getElementById('post_token_input').value.trim();
const pass = document.getElementById('post_token_password').value;
const pass2 = document.getElementById('post_token_password_confirmation').value;
const submitBtn = document.getElementById('postTokenSubmitBtn');
if (!email || !token || !pass) {
alert('Isi semua field: email, kode, dan password.');
event.preventDefault();
return false;
}
if (!/^\d{6}$/.test(token)) {
alert('Kode harus 6 digit angka.');
event.preventDefault();
return false;
}
if (pass.length < 8) {
alert('Password minimal 8 karakter.');
event.preventDefault();
return false;
}
if (pass !== pass2) {
alert('Konfirmasi password tidak cocok.');
event.preventDefault();
return false;
}
submitBtn.disabled = true;
submitBtn.textContent = '⏳ Memproses...';
return true;
}
// autofocus token input when panel is present
document.addEventListener('DOMContentLoaded', function () {
try {
const panel = document.getElementById('postSendTokenPanel');
// if there's a saved email from before submit, prefill and lock the email field
const savedEmail = (function(){ try{ return localStorage.getItem('password_reset_email') }catch(e){return null} })();
const emailInput = document.getElementById('post_token_email');
if (savedEmail && emailInput) {
emailInput.value = savedEmail;
emailInput.readOnly = true;
emailInput.classList.add('bg-gray-100');
}
if (panel) {
const t = document.getElementById('post_token_input');
if (t) t.focus();
}
} catch (e) {}
});
async function verifyCode() {
const email = document.getElementById('post_token_email').value.trim();
const token = document.getElementById('post_token_input').value.trim();
const verifyBtn = document.getElementById('verifyCodeBtn');
const submitBtn = document.getElementById('postTokenSubmitBtn');
const passwordFields = document.getElementById('passwordFields');
if (!email || !/^\d{6}$/.test(token)) {
alert('Masukkan email dan kode 6 digit yang valid terlebih dahulu.');
return;
}
verifyBtn.disabled = true;
verifyBtn.textContent = '⏳ Memeriksa...';
try {
const resp = await fetch('{{ route('password.verify.code') }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('input[name="_token"]').value,
'Accept': 'application/json'
},
body: JSON.stringify({email: email, token: token})
});
const data = await resp.json();
if (resp.ok && data.ok) {
// reveal password fields and enable submit
passwordFields.classList.remove('hidden');
submitBtn.disabled = false;
submitBtn.classList.remove('opacity-60','cursor-not-allowed');
verifyBtn.textContent = '✔️ Kode Valid';
verifyBtn.classList.add('bg-green-100');
// focus password
setTimeout(()=>{ document.getElementById('post_token_password').focus(); }, 50);
// clear stored email after successful verification
try { localStorage.removeItem('password_reset_email'); } catch(e){}
} else {
verifyBtn.disabled = false;
verifyBtn.textContent = '🔎 Verifikasi Kode';
alert(data.message || (data.errors || ['Token tidak valid'])[0]);
}
} catch (err) {
verifyBtn.disabled = false;
verifyBtn.textContent = '🔎 Verifikasi Kode';
alert('Terjadi kesalahan saat memverifikasi kode.');
}
}
</script>
<script>
function toggleTokenForm(e) {
e.preventDefault();
const panel = document.getElementById('tokenForm');
panel.classList.toggle('hidden');
const btn = document.getElementById('toggleTokenForm');
btn.textContent = panel.classList.contains('hidden') ? 'Gunakan token' : 'Sembunyikan';
}
function hideTokenForm() {
const panel = document.getElementById('tokenForm');
panel.classList.add('hidden');
const btn = document.getElementById('toggleTokenForm');
btn.textContent = 'Gunakan token';
}
function handleTokenSubmit(event) {
const email = document.getElementById('token_email').value.trim();
const token = document.getElementById('token_input').value.trim();
const pass = document.getElementById('token_password').value;
const pass2 = document.getElementById('token_password_confirmation').value;
const submitBtn = document.getElementById('tokenSubmitBtn');
if (!email || !token || !pass) {
alert('Isi semua field token, email, dan password.');
event.preventDefault();
return false;
}
if (pass.length < 8) {
alert('Password minimal 8 karakter.');
event.preventDefault();
return false;
}
if (pass !== pass2) {
alert('Konfirmasi password tidak cocok.');
event.preventDefault();
return false;
}
submitBtn.disabled = true;
submitBtn.textContent = '⏳ Memproses...';
return true;
}
</script>
</body>
</html>

View File

@ -405,6 +405,20 @@
</div>
@endif
{{-- Success Message: Password Reset Successfully --}}
@if (session('status'))
<div class="success-alert" style="background-color: #ECFDF5; border: 2px solid #A7F3D0; color: #065F46; padding: 12px 14px; border-radius: 8px; margin-bottom: 18px; font-size: 13px; font-weight: 500;">
<div style="display: flex; align-items: flex-start; gap: 10px;">
<span style="font-size: 20px;"></span>
<div>
<p style="margin: 0; font-weight: 700;">Password Berhasil Direset!</p>
<p style="margin: 5px 0 0 0; line-height: 1.5;">{{ session('status') }}</p>
<p style="margin: 8px 0 0 0; font-size: 12px; font-weight: 600;">Silakan login dengan password baru Anda.</p>
</div>
</div>
</div>
@endif
<form method="POST" action="{{ route('login') }}">
@csrf

View File

@ -81,12 +81,25 @@
</li>
@endforeach
</ul>
<p class="text-red-700 text-xs mt-3 font-semibold">💡 Pastikan:</p>
<ul class="text-red-700 text-xs mt-1 space-y-0.5 ml-2">
<li> Password minimal 8 karakter</li>
<li> Kedua password sama persis</li>
<li> Tidak menggunakan password lama</li>
</ul>
@if($errors->has('token'))
<div class="mt-4 pt-3 border-t border-red-200">
<p class="text-red-700 text-xs font-semibold mb-2"> Token telah kadaluarsa?</p>
<p class="text-red-700 text-xs mb-3">Minta tautan reset password yang baru:</p>
<a href="{{ route('password.request') }}" class="inline-block px-4 py-2 bg-red-600 text-white text-xs font-semibold rounded hover:bg-red-700 transition">
🔄 Minta Link Reset Baru
</a>
</div>
@endif
@if(!$errors->has('token'))
<p class="text-red-700 text-xs mt-3 font-semibold">💡 Pastikan:</p>
<ul class="text-red-700 text-xs mt-1 space-y-0.5 ml-2">
<li> Password minimal 8 karakter</li>
<li> Kedua password sama persis</li>
<li> Tidak menggunakan password lama</li>
</ul>
@endif
</div>
</div>
</div>

View File

@ -55,7 +55,7 @@
<td class="px-6 py-4 text-center gap-2 flex justify-center">
<a href="{{ route('bk.alumni.show', $a->id) }}" class="text-blue-600 hover:text-blue-800 font-semibold text-sm">👁 Lihat</a>
<a href="{{ route('bk.alumni.edit', $a->id) }}" class="text-yellow-600 hover:text-yellow-800 font-semibold text-sm"> Edit</a>
<form action="{{ route('bk.alumni.destroy', $a->id) }}" method="POST" class="inline" onsubmit="return confirm('Yakin hapus?')">
<form action="{{ route('bk.alumni.destroy', $a->id) }}" method="POST" class="inline swal-confirm" data-confirm-message="Yakin hapus?">
@csrf @method('DELETE')
<button type="submit" class="text-red-600 hover:text-red-800 font-semibold text-sm">🗑 Hapus</button>
</form>

View File

@ -77,7 +77,7 @@
<a href="{{ route('bk.alumni.edit', $alumni->id) }}" class="px-6 py-2 rounded-lg font-bold bg-yellow-400 text-maroon hover:bg-yellow-300 transition">
Edit
</a>
<form action="{{ route('bk.alumni.destroy', $alumni->id) }}" method="POST" class="inline" onsubmit="return confirm('Yakin hapus data alumni ini?')">
<form action="{{ route('bk.alumni.destroy', $alumni->id) }}" method="POST" class="inline swal-confirm" data-confirm-message="Yakin hapus data alumni ini?">
@csrf @method('DELETE')
<button type="submit" class="px-6 py-2 rounded-lg font-bold bg-red-500 text-white hover:bg-red-600 transition">
🗑 Hapus

View File

@ -10,7 +10,7 @@
<!-- 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">
<div class="stat-card bg-white rounded-lg shadow p-6 border-t-4 border-purple-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>
@ -47,13 +47,13 @@
</div>
</div>
<!-- Kelompok Distribution & Top Majors -->
<!-- Rekomendasi & Top Majors -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<!-- Kelompok Chart -->
<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>
<!-- Rekomendasi per Kelompok -->
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-purple-500">
<h3 class="text-lg font-bold text-bk mb-4">📊 Rekomendasi per Kelompok</h3>
<div style="position: relative; height: 250px;">
<canvas id="chartKelompokPie"></canvas>
<canvas id="chartRekomendasiKelompok"></canvas>
</div>
</div>
@ -72,7 +72,7 @@
@if($recentStudents->isNotEmpty())
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead class="border-b-2 border-teal-500">
<thead class="border-b-2 border-purple-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>
@ -91,7 +91,7 @@
</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>
<a href="{{ route('bk.student.detail', $student->id) }}" class="text-purple-600 hover:text-purple-800 font-semibold text-xs">👁 Lihat</a>
</td>
</tr>
@endforeach
@ -212,17 +212,18 @@
}
});
// Chart 3: Kelompok Pie Chart
const chartKelompokPieCtx = document.getElementById('chartKelompokPie').getContext('2d');
const chartKelompokPie = new Chart(chartKelompokPieCtx, {
type: 'pie',
// Chart 3: Rekomendasi per Kelompok Bar Chart
const chartRekomendasiKelompokCtx = document.getElementById('chartRekomendasiKelompok').getContext('2d');
const chartRekomendasiKelompok = new Chart(chartRekomendasiKelompokCtx, {
type: 'bar',
data: {
labels: @json($chartKelompokNames),
labels: @json($rekomendasiPerKelompok->pluck('kelompok_asal')->toArray()),
datasets: [{
data: @json($chartKelompokCounts),
label: 'Jumlah Rekomendasi',
data: @json($rekomendasiPerKelompok->pluck('count')->toArray()),
backgroundColor: ['#0369A1', '#D97706'],
borderColor: '#fff',
borderWidth: 2
borderColor: ['#0369A1', '#D97706'],
borderWidth: 1
}]
},
options: {
@ -230,10 +231,17 @@
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
display: true,
labels: {
font: { size: 11 },
padding: 10
font: { size: 11 }
}
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
font: { size: 10 }
}
}
}

View File

@ -72,58 +72,18 @@
</div>
</div>
<!-- Bobot Mata Pelajaran -->
<!-- Bobot Mata Pelajaran dihapus: gunakan bobot ROC global -->
<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_mapel[ipa][mtk]" value="{{ old('bobot_mapel.ipa.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:ring-2 focus:ring-teal-400 text-sm" oninput="validateBobot()">
</div>
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">Fisika</label>
<input type="number" name="bobot_mapel[ipa][fisika]" value="{{ old('bobot_mapel.ipa.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:ring-2 focus:ring-teal-400 text-sm" oninput="validateBobot()">
</div>
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">Kimia</label>
<input type="number" name="bobot_mapel[ipa][kimia]" value="{{ old('bobot_mapel.ipa.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:ring-2 focus:ring-teal-400 text-sm" oninput="validateBobot()">
</div>
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">Biologi</label>
<input type="number" name="bobot_mapel[ipa][biologi]" value="{{ old('bobot_mapel.ipa.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:ring-2 focus:ring-teal-400 text-sm" oninput="validateBobot()">
</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_mapel[ips][ekonomi]" value="{{ old('bobot_mapel.ips.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:ring-2 focus:ring-teal-400 text-sm" oninput="validateBobot()">
</div>
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">Geografi</label>
<input type="number" name="bobot_mapel[ips][geografi]" value="{{ old('bobot_mapel.ips.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:ring-2 focus:ring-teal-400 text-sm" oninput="validateBobot()">
</div>
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">Sosiologi</label>
<input type="number" name="bobot_mapel[ips][sosiologi]" value="{{ old('bobot_mapel.ips.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:ring-2 focus:ring-teal-400 text-sm" oninput="validateBobot()">
</div>
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">Sejarah</label>
<input type="number" name="bobot_mapel[ips][sejarah]" value="{{ old('bobot_mapel.ips.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:ring-2 focus:ring-teal-400 text-sm" oninput="validateBobot()">
</div>
</div>
</div>
</div>
<div id="bobotInfo" class="mt-4 p-3 bg-blue-50 border border-blue-200 rounded-lg hidden">
<p class="text-blue-700 text-xs"><span id="bobotTotal">0</span> / 2.00 (Total bobot untuk penilaian)</p>
</div>
<h3 class="text-lg font-bold text-bk mb-4">⚖️ Bobot Penilaian (ROC)</h3>
<p class="text-sm text-gray-600 mb-3">Mulai sekarang, sistem menggunakan bobot ROC tetap untuk semua jurusan. Pengaturan bobot per-mata-pelajaran tidak lagi tersedia di form ini.</p>
<ul class="list-disc pl-5 text-sm text-gray-700 space-y-1">
<li><strong>Minat</strong>: 45.6% (0.456)</li>
<li><strong>Preferensi</strong>: 25.6% (0.256)</li>
<li><strong>Nilai</strong>: 15.6% (0.156)</li>
<li><strong>Cita-cita</strong>: 9.0% (0.090)</li>
<li><strong>Prestasi</strong>: 4.0% (0.040)</li>
</ul>
<p class="text-xs text-gray-500 mt-3">Catatan: Jika Anda tetap menyimpan nilai bobot di database, sistem akan mengabaikannya dan menggunakan bobot ROC global dalam perhitungan rekomendasi.</p>
</div>
<div class="flex gap-4">
@ -172,32 +132,8 @@ function validateJurusanForm() {
submitButton.disabled = !isNamaValid;
}
function validateBobot() {
const bobotInputs = document.querySelectorAll('input[type="number"][name^="bobot_"]');
const bobotInfo = document.getElementById('bobotInfo');
const bobotTotal = document.getElementById('bobotTotal');
let total = 0;
bobotInputs.forEach(input => {
let value = parseFloat(input.value);
if (isNaN(value) || value < 0) {
value = 0;
input.value = '0.00';
}
if (value > 1) {
value = 1;
input.value = '1.00';
}
total += value;
});
bobotTotal.textContent = total.toFixed(2);
bobotInfo.classList.remove('hidden');
}
document.addEventListener('DOMContentLoaded', function() {
validateJurusanForm();
validateBobot();
updateCharCount('deskripsi');
});
</script>

View File

@ -37,7 +37,7 @@
<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> <span class="text-gray-400 text-xs">(minimal 3 karakter)</span></label>
<input type="text" name="nama_jurusan" value="{{ old('nama_jurusan', $jurusan->nama_jurusan) }}" minlength="3" maxlength="100" class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 transition @error('nama_jurusan') border-red-500 focus:ring-red-400 @else border-gray-300 focus:ring-teal-400 @enderror" required oninput="validateJurusanForm()">
<input type="text" name="nama_jurusan" value="{{ old('nama_jurusan', $jurusan->nama_jurusan) }}" minlength="3" maxlength="100" class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 transition @error('nama_jurusan') border-red-500 focus:ring-red-400 @else border-gray-300 focus:ring-teal-400 @enderror" placeholder="Contoh: Teknologi Informasi" required oninput="validateJurusanForm()">
<div class="flex justify-between items-center mt-1">
<span id="namaError" class="text-red-500 text-xs hidden">⚠️ Nama harus minimal 3 karakter</span>
<span id="namaValid" class="text-green-500 text-xs hidden"> Nama valid</span>
@ -80,63 +80,18 @@
</div>
</div>
<!-- Bobot Mata Pelajaran -->
@php
$bobot = $jurusan->bobot_mapel ?? [];
$bobotIpa = data_get($bobot, 'ipa', $bobot);
$bobotIps = data_get($bobot, 'ips', $bobot);
@endphp
<!-- Bobot Mata Pelajaran dihapus: gunakan bobot ROC global -->
<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_mapel[ipa][mtk]" value="{{ old('bobot_mapel.ipa.mtk', data_get($bobotIpa, 'mtk', data_get($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:ring-2 focus:ring-teal-400 text-sm" oninput="validateBobot()">
</div>
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">Fisika</label>
<input type="number" name="bobot_mapel[ipa][fisika]" value="{{ old('bobot_mapel.ipa.fisika', data_get($bobotIpa, 'fisika', data_get($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:ring-2 focus:ring-teal-400 text-sm" oninput="validateBobot()">
</div>
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">Kimia</label>
<input type="number" name="bobot_mapel[ipa][kimia]" value="{{ old('bobot_mapel.ipa.kimia', data_get($bobotIpa, 'kimia', data_get($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:ring-2 focus:ring-teal-400 text-sm" oninput="validateBobot()">
</div>
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">Biologi</label>
<input type="number" name="bobot_mapel[ipa][biologi]" value="{{ old('bobot_mapel.ipa.biologi', data_get($bobotIpa, 'biologi', data_get($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:ring-2 focus:ring-teal-400 text-sm" oninput="validateBobot()">
</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_mapel[ips][ekonomi]" value="{{ old('bobot_mapel.ips.ekonomi', data_get($bobotIps, 'ekonomi', data_get($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:ring-2 focus:ring-teal-400 text-sm" oninput="validateBobot()">
</div>
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">Geografi</label>
<input type="number" name="bobot_mapel[ips][geografi]" value="{{ old('bobot_mapel.ips.geografi', data_get($bobotIps, 'geografi', data_get($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:ring-2 focus:ring-teal-400 text-sm" oninput="validateBobot()">
</div>
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">Sosiologi</label>
<input type="number" name="bobot_mapel[ips][sosiologi]" value="{{ old('bobot_mapel.ips.sosiologi', data_get($bobotIps, 'sosiologi', data_get($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:ring-2 focus:ring-teal-400 text-sm" oninput="validateBobot()">
</div>
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">Sejarah</label>
<input type="number" name="bobot_mapel[ips][sejarah]" value="{{ old('bobot_mapel.ips.sejarah', data_get($bobotIps, 'sejarah', data_get($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:ring-2 focus:ring-teal-400 text-sm" oninput="validateBobot()">
</div>
</div>
</div>
</div>
<div id="bobotInfo" class="mt-4 p-3 bg-blue-50 border border-blue-200 rounded-lg hidden">
<p class="text-blue-700 text-xs"><span id="bobotTotal">0</span> / 2.00 (Total bobot untuk penilaian)</p>
</div>
<h3 class="text-lg font-bold text-bk mb-4">⚖️ Bobot Penilaian (ROC)</h3>
<p class="text-sm text-gray-600 mb-3">Mulai sekarang, sistem menggunakan bobot ROC tetap untuk semua jurusan. Pengaturan bobot per-mata-pelajaran tidak lagi tersedia di form ini.</p>
<ul class="list-disc pl-5 text-sm text-gray-700 space-y-1">
<li><strong>Minat</strong>: 45.6% (0.456)</li>
<li><strong>Preferensi</strong>: 25.6% (0.256)</li>
<li><strong>Nilai</strong>: 15.6% (0.156)</li>
<li><strong>Cita-cita</strong>: 9.0% (0.090)</li>
<li><strong>Prestasi</strong>: 4.0% (0.040)</li>
</ul>
<p class="text-xs text-gray-500 mt-3">Catatan: Jika Anda tetap menyimpan nilai bobot di database, sistem akan mengabaikannya dan menggunakan bobot ROC global dalam perhitungan rekomendasi.</p>
</div>
<div class="flex gap-4">
@ -185,32 +140,8 @@ function validateJurusanForm() {
submitButton.disabled = !isNamaValid;
}
function validateBobot() {
const bobotInputs = document.querySelectorAll('input[type="number"][name^="bobot_"]');
const bobotInfo = document.getElementById('bobotInfo');
const bobotTotal = document.getElementById('bobotTotal');
let total = 0;
bobotInputs.forEach(input => {
let value = parseFloat(input.value);
if (isNaN(value) || value < 0) {
value = 0;
input.value = '0.00';
}
if (value > 1) {
value = 1;
input.value = '1.00';
}
total += value;
});
bobotTotal.textContent = total.toFixed(2);
bobotInfo.classList.remove('hidden');
}
document.addEventListener('DOMContentLoaded', function() {
validateJurusanForm();
validateBobot();
updateCharCount('deskripsi');
});
</script>

View File

@ -70,7 +70,7 @@
<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 }}?')">
<form action="{{ route('bk.jurusan.destroy', $jurusan->id) }}" method="POST" class="swal-confirm" data-confirm-message="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">

View File

@ -7,18 +7,18 @@
<title>@yield('title', 'Panel Guru BK') - SPK Jurusan Polije</title>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<style>
/* Keep BK theme, but also support admin-style utility classes used by Alumni pages */
.gradient-maroon { background: linear-gradient(135deg, #6B7280 0%, #8B95A5 100%); }
.text-maroon { color: #6B7280; }
.border-maroon { border-color: #6B7280; }
.bg-maroon { background-color: #6B7280; }
/* Updated BK theme to match modern purple-indigo gradient */
.gradient-maroon { background: linear-gradient(135deg, #7c3aed 0%, #6366f1 100%); }
.text-maroon { color: #7c3aed; }
.border-maroon { border-color: #7c3aed; }
.bg-maroon { background-color: #7c3aed; }
.gradient-bk { background: linear-gradient(135deg, #5A8A7F 0%, #7BA39A 100%); }
.text-bk { color: #5A8A7F; }
.border-bk { border-color: #5A8A7F; }
.bg-cream { background-color: #F8FAFC; }
.gradient-bk { background: linear-gradient(135deg, #7c3aed 0%, #6366f1 100%); }
.text-bk { color: #7c3aed; }
.border-bk { border-color: #7c3aed; }
.bg-cream { background-color: #f8fafc; }
.stat-card { transition: all 0.3s ease; }
.stat-card:hover { transform: translateY(-5px); box-shadow: 0 5px 15px rgba(90, 138, 127, 0.1); }
.stat-card:hover { transform: translateY(-5px); box-shadow: 0 5px 15px rgba(124, 58, 237, 0.1); }
/* Sidebar */
.sidebar-dark {
@ -31,14 +31,14 @@
color: #cbd5e1;
}
.sidebar-link:hover {
background: rgba(90, 138, 127, 0.12);
background: rgba(124, 58, 237, 0.12);
color: #ffffff;
border-left-color: rgba(90, 138, 127, 0.5);
border-left-color: rgba(124, 58, 237, 0.5);
}
.sidebar-link.active {
background: linear-gradient(90deg, rgba(90,138,127,0.2) 0%, rgba(90,138,127,0.03) 100%);
color: #a8bfb8 !important;
border-left-color: #a8bfb8;
background: linear-gradient(90deg, rgba(124, 58, 237, 0.2) 0%, rgba(124, 58, 237, 0.03) 100%);
color: #c4b5fd !important;
border-left-color: #c4b5fd;
}
.sidebar-link .sidebar-icon {
display: inline-flex;
@ -53,11 +53,11 @@
transition: all 0.25s ease;
}
.sidebar-link:hover .sidebar-icon {
background: rgba(90, 138, 127, 0.25);
background: rgba(124, 58, 237, 0.25);
transform: scale(1.05);
}
.sidebar-link.active .sidebar-icon {
background: rgba(90, 138, 127, 0.15);
background: rgba(124, 58, 237, 0.15);
}
.sidebar-section-label {
font-size: 10px;
@ -77,13 +77,13 @@
.sidebar-brand-icon {
width: 40px;
height: 40px;
background: linear-gradient(135deg, #5A8A7F 0%, #7BA39A 100%);
background: linear-gradient(135deg, #7c3aed 0%, #6366f1 100%);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
box-shadow: 0 4px 12px rgba(90, 138, 127, 0.3);
box-shadow: 0 4px 12px rgba(124, 58, 237, 0.3);
}
.sidebar-footer {
border-top: 1px solid rgba(255,255,255,0.06);
@ -123,7 +123,7 @@
<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') }}">
<form method="POST" action="{{ route('logout') }}" class="confirm-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
@ -272,6 +272,50 @@
mobileOverlay.addEventListener('click', () => mobileSidebar.classList.add('hidden'));
closeMobileMenu.addEventListener('click', () => mobileSidebar.classList.add('hidden'));
</script>
<!-- SweetAlert2 (BK) -->
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('form.confirm-logout').forEach(function(form) {
form.addEventListener('submit', function (e) {
e.preventDefault();
Swal.fire({
title: 'Konfirmasi Logout',
text: 'Yakin ingin logout dari akun ini?',
icon: 'question',
showCancelButton: true,
confirmButtonText: 'Ya, Logout',
cancelButtonText: 'Batal',
reverseButtons: true
}).then(function(result) {
if (result.isConfirmed) {
form.submit();
}
});
});
// Generic SweetAlert2 confirmation for destructive actions in BK
document.querySelectorAll('form.swal-confirm').forEach(function(form) {
form.addEventListener('submit', function(e) {
e.preventDefault();
const msg = form.getAttribute('data-confirm-message') || 'Yakin melanjutkan aksi ini?';
Swal.fire({
title: 'Konfirmasi',
text: msg,
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Ya',
cancelButtonText: 'Batal',
reverseButtons: true
}).then(function(result) {
if (result.isConfirmed) {
form.submit();
}
});
});
});
});
});
</script>
@yield('scripts')
</body>
</html>

View File

@ -50,7 +50,7 @@
<a href="{{ route('profile.edit') }}" class="block px-4 py-3 hover:bg-gray-50 text-xs sm:text-sm font-semibold border-b border-gray-100 rounded-t-lg">
👤 Lihat Profil
</a>
<form method="POST" action="{{ route('logout') }}">
<form method="POST" action="{{ route('logout') }}" class="confirm-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
@ -272,5 +272,29 @@
}
});
</script>
<!-- SweetAlert2 for nicer logout modal on dashboard -->
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('form.confirm-logout').forEach(function(form) {
form.addEventListener('submit', function (e) {
e.preventDefault();
Swal.fire({
title: 'Konfirmasi Logout',
text: 'Yakin ingin logout dari akun ini?',
icon: 'question',
showCancelButton: true,
confirmButtonText: 'Ya, Logout',
cancelButtonText: 'Batal',
reverseButtons: true
}).then(function(result) {
if (result.isConfirmed) {
form.submit();
}
});
});
});
});
</script>
</body>
</html>

View File

@ -204,6 +204,14 @@
<a href="{{ $resetUrl }}" class="cta-button">🔑 RESET PASSWORD</a>
</div>
<!-- Code alternative -->
@if(!empty($code))
<div style="margin-top:18px;">
<p style="font-size:14px;color:#374151;margin-bottom:8px;">Atau, jika Anda ingin melakukan reset tanpa membuka tautan, salin <strong>kode token 6 digit</strong> di bawah ini dan masukkan pada form "Gunakan token" di halaman Lupa Password:</p>
<div style="background:#f3f4f6;border:1px dashed #e5e7eb;padding:14px;border-radius:6px;font-family:monospace;color:#111827;font-weight:700;word-break:break-all;font-size:20px;letter-spacing:4px;text-align:center;">{{ $code }}</div>
</div>
@endif
<!-- Steps -->
<div class="steps">
<h3>📋 Langkah-Langkah:</h3>

View File

@ -32,5 +32,49 @@
{{ $slot }}
</main>
</div>
<!-- SweetAlert2 for nicer confirmation modals -->
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('form.confirm-logout').forEach(function(form) {
form.addEventListener('submit', function (e) {
e.preventDefault();
Swal.fire({
title: 'Konfirmasi Logout',
text: 'Yakin ingin logout dari akun ini?',
icon: 'question',
showCancelButton: true,
confirmButtonText: 'Ya, Logout',
cancelButtonText: 'Batal',
reverseButtons: true
}).then(function(result) {
if (result.isConfirmed) {
form.submit();
}
});
});
});
// Generic SweetAlert2 confirmation for destructive actions
document.querySelectorAll('form.swal-confirm').forEach(function(form) {
form.addEventListener('submit', function(e) {
e.preventDefault();
const msg = form.getAttribute('data-confirm-message') || 'Yakin melanjutkan aksi ini?';
Swal.fire({
title: 'Konfirmasi',
text: msg,
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Ya',
cancelButtonText: 'Batal',
reverseButtons: true
}).then(function(result) {
if (result.isConfirmed) {
form.submit();
}
});
});
});
});
</script>
</body>
</html>

View File

@ -39,7 +39,7 @@
</x-dropdown-link>
<!-- Authentication -->
<form method="POST" action="{{ route('logout') }}">
<form method="POST" action="{{ route('logout') }}" class="confirm-logout">
@csrf
<x-dropdown-link :href="route('logout')"
@ -85,7 +85,7 @@
</x-responsive-nav-link>
<!-- Authentication -->
<form method="POST" action="{{ route('logout') }}">
<form method="POST" action="{{ route('logout') }}" class="confirm-logout">
@csrf
<x-responsive-nav-link :href="route('logout')"

View File

@ -6,461 +6,292 @@
<title>Hasil Rekomendasi - Sistem Pemilihan Jurusan</title>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<style>
.gradient-maroon {
background: linear-gradient(135deg, #6B7280 0%, #8B95A5 100%);
:root {
--primary: #0f766e;
--primary-light: #0d9488;
--accent: #f59e0b;
--success: #10b981;
--error: #ef4444;
--bg-soft: #f0f9ff;
--bg-card: #ffffff;
--text-main: #1f2937;
--text-secondary: #6b7280;
--border-light: #e5e7eb;
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.08);
}
.text-maroon {
color: #6B7280;
body {
background-color: var(--bg-soft);
color: var(--text-main);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
.border-maroon {
border-color: #6B7280;
.header-main {
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-light) 100%);
color: white;
position: sticky;
top: 0;
z-index: 50;
box-shadow: var(--shadow-md);
}
.bg-cream {
background-color: #F8FAFC;
.btn-primary {
background-color: var(--primary);
color: white;
transition: all 0.3s ease;
}
.bg-maroon-light {
background-color: rgba(107, 114, 128, 0.1);
.btn-primary:hover {
background-color: var(--primary-light);
box-shadow: var(--shadow-lg);
}
.card {
background: var(--bg-card);
border-radius: 12px;
box-shadow: var(--shadow-md);
border: 1px solid var(--border-light);
}
.card-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1.5rem;
}
.badge-primary {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 2.25rem;
min-height: 2.25rem;
background-color: var(--accent);
color: white;
border-radius: 9999px;
font-weight: 600;
font-size: 0.875rem;
}
.text-primary {
color: var(--primary);
}
.profile-box {
background: linear-gradient(135deg, rgba(15, 118, 110, 0.05) 0%, rgba(13, 148, 136, 0.05) 100%);
border: 1px solid rgba(15, 118, 110, 0.2);
border-radius: 12px;
padding: 1rem;
}
.result-item {
background: var(--bg-card);
border: 1px solid var(--border-light);
border-radius: 12px;
padding: 1.25rem;
transition: all 0.3s ease;
}
.result-item:hover {
box-shadow: var(--shadow-lg);
border-color: var(--primary);
}
.result-item.top-1 {
border-left: 4px solid var(--accent);
background: linear-gradient(to right, rgba(245, 158, 11, 0.05), white);
}
.progress-bar {
background-color: #e5e7eb;
border-radius: 9999px;
height: 0.5rem;
overflow: hidden;
}
.progress-fill {
background: linear-gradient(90deg, var(--primary) 0%, var(--primary-light) 100%);
height: 100%;
border-radius: 9999px;
transition: width 0.6s ease;
}
</style>
</head>
<body class="bg-cream">
<body>
<!-- Header -->
<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>
<h1 class="text-xl sm:text-2xl md:text-3xl font-bold">Hasil Rekomendasi</h1>
<p class="text-xs sm:text-sm text-yellow-300 font-semibold mt-1">Sistem Pemilihan Jurusan</p>
</div>
<div class="flex items-center gap-2 sm:gap-4 w-full sm:w-auto">
<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 ke Dashboard
</a>
<header class="header-main py-4 md:py-6">
<div class="container mx-auto px-4 md:px-6">
<div class="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
<div>
<h1 class="text-2xl md:text-3xl font-bold">Hasil Analisis Rekomendasi</h1>
<p class="text-sm md:text-base text-teal-100 mt-1">Jurusan terbaik berdasarkan profil Anda</p>
</div>
<div class="flex gap-3">
<a href="{{ route('rekomendasi.input') }}" class="btn-primary px-4 py-2 rounded-lg font-medium text-sm hover:shadow-lg transition">
🔄 Analisis Ulang
</a>
<a href="{{ url('/dashboard') }}" class="px-4 py-2 rounded-lg font-medium text-sm bg-white text-primary hover:bg-gray-50 transition">
Dashboard
</a>
</div>
</div>
</div>
</header>
<!-- Main Content -->
<div class="w-full px-4 sm:px-6 py-6 sm:py-8">
<!-- Selamat Section -->
<div class="bg-gradient-to-r from-green-600 to-emerald-600 rounded-xl shadow-lg p-8 text-white mb-8">
<div class="flex gap-4 items-start">
<div class="text-5xl">🎉</div>
<div>
<h2 class="text-3xl font-bold mb-2">Hasil Analisis Siap!</h2>
<p class="text-lg text-green-100 mb-4">
Sistem AI kami telah menganalisis profil kamu secara menyeluruh. Berikut adalah 9 program studi yang kami rekomendasikan, diurutkan dari yang paling sesuai dengan potensi dan minat kamu. Setiap jurusan memiliki skor kesesuaian yang menunjukkan tingkat kecocokan dengan profil kamu.
</p>
<div class="inline-block bg-white bg-opacity-20 rounded-lg px-4 py-2 backdrop-blur-sm">
<p class="text-sm font-semibold">💡 Tip: Cek beberapa program teratas dan diskusikan dengan konselor atau orang tua untuk memastikan pilihan terbaik untuk masa depanmu</p>
</div>
<main class="container mx-auto px-4 md:px-6 py-8 md:py-12">
<!-- Data Profil Summary -->
<div class="card p-6 md:p-8 mb-8">
<h2 class="text-2xl font-bold text-primary mb-6">Data Profil Anda</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- Nilai Akademik -->
<div class="profile-box">
<p style="font-size: 0.75rem; color: var(--text-secondary); text-transform: uppercase; font-weight: 600; margin-bottom: 0.5rem;">Nilai Akademik</p>
<p style="font-size: 1.5rem; font-weight: 700; color: var(--primary); margin-bottom: 0.25rem;">{{ $katNilai }}</p>
<p style="font-size: 0.875rem; color: var(--text-secondary);">Rata-rata: {{ number_format($average, 1) }}%</p>
</div>
</div>
</div>
<!-- Ringkasan Input -->
<div class="bg-white rounded-xl shadow-lg p-8 mb-8 border-t-4 border-purple-600">
<h3 class="text-2xl font-bold text-gray-900 mb-6">📊 Profil Analisismu</h3>
<div class="grid grid-cols-2 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div class="bg-gradient-to-br from-purple-50 to-purple-100 p-5 rounded-lg border border-purple-200">
<p class="text-xs font-semibold text-purple-600 mb-2">📚 Nilai Akademik</p>
<p class="text-2xl font-bold text-purple-900">{{ number_format($average, 1) }}</p>
<p class="text-xs text-purple-700 mt-1">Rata-rata Rapor</p>
<!-- Minat -->
<div class="profile-box">
<p style="font-size: 0.75rem; color: var(--text-secondary); text-transform: uppercase; font-weight: 600; margin-bottom: 0.5rem;">Bidang Minat</p>
<p style="font-size: 1.125rem; font-weight: 700; color: var(--primary);">{{ $minatMapped ?? '-' }}</p>
<p style="font-size: 0.75rem; color: var(--text-secondary); margin-top: 0.375rem;">Anda inputkan: <strong>{{ ucfirst($minatRaw ?? '-') }}</strong></p>
</div>
<div class="bg-gradient-to-br from-blue-50 to-blue-100 p-5 rounded-lg border border-blue-200">
<p class="text-xs font-semibold text-blue-600 mb-2">🎯 Preferensi Studi</p>
<p class="text-lg font-bold text-blue-900">{{ $prefStudi ?? '-' }}</p>
<p class="text-xs text-blue-700 mt-1">Gaya Belajar</p>
<!-- Cita-cita -->
<div class="profile-box">
<p style="font-size: 0.75rem; color: var(--text-secondary); text-transform: uppercase; font-weight: 600; margin-bottom: 0.5rem;">Cita-cita</p>
<p style="font-size: 1.125rem; font-weight: 700; color: var(--primary);">{{ $citaMapped ?? '-' }}</p>
<p style="font-size: 0.75rem; color: var(--text-secondary); margin-top: 0.375rem;">Anda inputkan: <strong>{{ ucfirst($citaRaw ?? '-') }}</strong></p>
</div>
<div class="bg-gradient-to-br from-amber-50 to-amber-100 p-5 rounded-lg border border-amber-200">
<p class="text-xs font-semibold text-amber-600 mb-2">🏆 Prestasi</p>
<p class="text-lg font-bold text-amber-900">
<!-- Preferensi Studi -->
<div class="profile-box">
<p style="font-size: 0.75rem; color: var(--text-secondary); text-transform: uppercase; font-weight: 600; margin-bottom: 0.5rem;">Preferensi Studi</p>
<p style="font-size: 1.125rem; font-weight: 700; color: var(--primary);">{{ $prefStudi ?? '-' }}</p>
</div>
<!-- Prestasi -->
<div class="profile-box">
<p style="font-size: 0.75rem; color: var(--text-secondary); text-transform: uppercase; font-weight: 600; margin-bottom: 0.5rem;">Prestasi</p>
<p style="font-size: 1.125rem; font-weight: 700; color: var(--primary);">
@if(!($isPrestasiFilled ?? true))
Belum Ada
Tidak Ada
@elseif($prestasiScore >= 0.8)
Tinggi
Tinggi
@elseif($prestasiScore >= 0.6)
Sedang
Sedang 👍
@elseif($prestasiScore > 0)
Cukup
@else
Belum Ada
@endif
</p>
<p class="text-xs text-amber-700 mt-1">Pencapaian</p>
</div>
<div class="bg-gradient-to-br from-green-50 to-green-100 p-5 rounded-lg border border-green-200">
<p class="text-xs font-semibold text-green-600 mb-2">💯 Skor Nilai</p>
<p class="text-2xl font-bold text-green-900">{{ number_format($average, 1) }}%</p>
<p class="text-xs text-green-700 mt-1">Nilai Rata-rata</p>
</div>
</div>
<p class="text-xs sm:text-sm text-gray-600">Minat</p>
<p class="text-sm sm:text-lg font-bold text-maroon">{{ $minatMapped ?? '-' }}</p>
<p class="text-xs text-gray-500 mt-1">Input: {{ ucfirst($minatRaw ?? '-') }}</p>
</div>
<div class="bg-yellow-50 p-3 sm:p-4 rounded-lg">
<p class="text-xs sm:text-sm text-gray-600">Cita-cita</p>
<p class="text-sm sm:text-lg font-bold text-maroon">{{ $citaMapped ?? '-' }}</p>
<p class="text-xs text-gray-500 mt-1">Input: {{ ucfirst($citaRaw ?? '-') }}</p>
</div>
</div>
</div>
<!-- Tabel Peringkat Jurusan -->
<div class="bg-white rounded-lg shadow-lg p-5 sm:p-6 mb-6 sm:mb-8">
<h3 class="text-base sm:text-lg font-bold text-maroon mb-3 sm:mb-4">Peringkat Jurusan</h3>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead class="gradient-maroon text-white">
<tr>
<th class="px-3 sm:px-6 py-2 sm:py-3 text-left text-xs sm:text-sm font-bold">#</th>
<th class="px-3 sm:px-6 py-2 sm:py-3 text-left text-xs sm:text-sm font-bold">Jurusan</th>
<th class="px-3 sm:px-6 py-2 sm:py-3 text-right text-xs sm:text-sm font-bold">Skor</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
@foreach($hasilAkhir as $index => $res)
<tr class="hover:bg-gray-50 transition {{ $index == 0 ? 'bg-yellow-50' : '' }}">
<td class="px-3 sm:px-6 py-2 sm:py-4 text-center">
<span class="inline-flex items-center justify-center w-6 h-6 sm:w-8 sm:h-8 rounded-full text-xs sm:text-sm font-bold {{ $index == 0 ? 'bg-yellow-400 text-white' : 'bg-gray-200 text-gray-700' }}">
{{ $index + 1 }}
</span>
</td>
<td class="px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm font-semibold text-gray-900">
{{ $res['jurusan'] ?? '-' }}
@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">
Utama
</span>
@endif
</td>
<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'] ?? 0) * 100, 1) }}%
</td>
</tr>
@endforeach
</tbody>
</table>
<!-- Top Recommendation -->
@if(count($hasilAkhir) > 0)
<div class="mb-8">
<div class="result-item top-1 border-4">
<div class="flex items-start justify-between mb-4">
<div>
<span style="display: inline-block; background: var(--accent); color: white; padding: 0.25rem 0.75rem; border-radius: 9999px; font-size: 0.75rem; font-weight: 600; margin-bottom: 0.75rem;">
🏆 REKOMENDASI UTAMA
</span>
<h3 style="font-size: 2rem; font-weight: 700; color: var(--primary);">{{ $hasilAkhir[0]['jurusan'] ?? '-' }}</h3>
</div>
<div style="text-align: right;">
<p style="font-size: 2.5rem; font-weight: 700; color: var(--accent);">{{ number_format(($hasilAkhir[0]['skor'] ?? 0) * 100, 1) }}%</p>
<p style="font-size: 0.875rem; color: var(--text-secondary);">Skor Kesesuaian</p>
</div>
</div>
<div class="progress-bar mb-4">
<div class="progress-fill" style="width: {{ number_format(($hasilAkhir[0]['skor'] ?? 0) * 100, 1) }}%"></div>
</div>
<p style="font-size: 0.875rem; color: var(--text-secondary); margin-bottom: 1rem;">
Berdasarkan analisis Weighted Naive Bayes, jurusan ini adalah pilihan terbaik untuk profil akademik dan minat Anda.
</p>
</div>
<!-- Visualisasi Progress Bars -->
<div class="mt-4 sm:mt-6 space-y-2 sm:space-y-3">
</div>
@endif
<!-- All Rankings -->
<div class="card p-6 md:p-8">
<h2 class="text-2xl font-bold text-primary mb-6">Peringkat Semua Jurusan</h2>
<div class="space-y-4">
@foreach($hasilAkhir as $index => $res)
<div class="flex flex-col gap-1">
<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-bold text-maroon">{{ number_format(($res['skor'] ?? 0) * 100, 1) }}%</span>
<div class="result-item {{ $index == 0 ? 'top-1' : '' }}">
<div class="flex items-start justify-between mb-3">
<div class="flex items-center gap-4">
<div class="badge-primary">{{ $index + 1 }}</div>
<div>
<p style="font-weight: 600; color: var(--text-main); margin-bottom: 0.125rem;">{{ $res['jurusan'] ?? '-' }}</p>
<p style="font-size: 0.875rem; color: var(--text-secondary);">
@if($index == 0)
Pilihan terbaik untuk Anda
@elseif($index < 3)
Rekomendasi alternatif yang baik
@else
Pilihan lainnya
@endif
</p>
</div>
</div>
<div style="text-align: right;">
<p style="font-size: 1.5rem; font-weight: 700; color: var(--primary);">
{{ number_format(($res['skor'] ?? 0) * 100, 1) }}%
</p>
</div>
</div>
<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'] ?? 0) * 100, 1) }}%"></div>
<div class="progress-bar">
<div class="progress-fill" style="width: {{ number_format(($res['skor'] ?? 0) * 100, 1) }}%"></div>
</div>
</div>
@endforeach
</div>
</div>
<!-- Detail Rekomendasi Utama -->
@if(count($hasilAkhir) > 0)
<div class="bg-white rounded-lg shadow-lg p-5 sm:p-8 mb-6 sm:mb-8 border-l-4 border-yellow-400">
@php
$topRecommendation = $hasilAkhir[0];
$detail = $topRecommendation['detail'] ?? [];
@endphp
<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>
<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">
Skor Kesesuaian: <span class="font-bold text-maroon">{{ number_format(($topRecommendation['skor'] ?? 0) * 100, 1) }}%</span>
</p>
@if($topJurusan && $topJurusan->deskripsi)
<p class="text-xs sm:text-sm text-gray-600 mt-2">{{ $topJurusan->deskripsi }}</p>
@endif
</div>
</div>
<!-- Analysis Breakdown -->
<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">Likelihood per Kriteria (Weighted Naive Bayes):</h4>
<div class="space-y-2 sm:space-y-3">
<div>
<div class="flex justify-between items-center mb-1">
<p class="text-xs sm:text-sm font-semibold text-gray-700">Nilai Akademik P(nilai|H) &times; w=0.156</p>
<span class="text-xs sm:text-sm font-bold text-maroon">{{ number_format(($detail['nilai'] ?? 0) * 100, 1) }}%</span>
</div>
<div class="w-full bg-gray-300 rounded-full h-2">
<div class="gradient-maroon h-2 rounded-full" style="width: {{ number_format(($detail['nilai'] ?? 0) * 100, 1) }}%"></div>
</div>
</div>
<div>
<div class="flex justify-between items-center mb-1">
<p class="text-xs sm:text-sm font-semibold text-gray-700">Minat & Bakat P(minat|H) &times; w=0.456</p>
<span class="text-xs sm:text-sm font-bold text-maroon">{{ number_format(($detail['minat'] ?? 0) * 100, 1) }}%</span>
</div>
<div class="w-full bg-gray-300 rounded-full h-2">
<div class="bg-yellow-400 h-2 rounded-full" style="width: {{ number_format(($detail['minat'] ?? 0) * 100, 1) }}%"></div>
</div>
</div>
<div>
<div class="flex justify-between items-center mb-1">
<p class="text-xs sm:text-sm font-semibold text-gray-700">Preferensi Studi P(pref|H) &times; w=0.256</p>
<span class="text-xs sm:text-sm font-bold text-maroon">{{ number_format(($detail['pref'] ?? 0) * 100, 1) }}%</span>
</div>
<div class="w-full bg-gray-300 rounded-full h-2">
<div class="gradient-maroon h-2 rounded-full" style="width: {{ number_format(($detail['pref'] ?? 0) * 100, 1) }}%"></div>
</div>
</div>
<div>
<div class="flex justify-between items-center mb-1">
<p class="text-xs sm:text-sm font-semibold text-gray-700">Cita-cita P(cita|H) &times; w=0.090</p>
<span class="text-xs sm:text-sm font-bold text-maroon">
{{ number_format(($detail['cita'] ?? 0) * 100, 1) }}%
</span>
</div>
<div class="w-full bg-gray-300 rounded-full h-2">
<div class="bg-yellow-400 h-2 rounded-full" style="width: {{ number_format(($detail['cita'] ?? 0) * 100, 1) }}%"></div>
</div>
</div>
<div>
<div class="flex justify-between items-center mb-1">
<p class="text-xs sm:text-sm font-semibold text-gray-700">
Prestasi P(prestasi|H) &times; w={{ ($isPrestasiFilled ?? true) ? '0.040' : '0.00' }}
</p>
<span class="text-xs sm:text-sm font-bold text-maroon">
@if(!($isPrestasiFilled ?? true))
Tidak dihitung
@else
{{ number_format(($detail['prestasi'] ?? 0) * 100, 1) }}%
@endif
</span>
</div>
<div class="w-full bg-gray-300 rounded-full h-2">
<div class="gradient-maroon h-2 rounded-full" style="width: {{ number_format(($detail['prestasi'] ?? 0) * 100, 1) }}%"></div>
</div>
</div>
</div>
</div>
<!-- Explanation Breakdown -->
<div class="mb-4 sm:mb-6 p-3 sm:p-4 bg-blue-50 rounded-lg border border-blue-200">
<h4 class="font-bold text-blue-900 text-sm sm:text-base mb-3 sm:mb-4 flex items-center gap-2">
<span class="text-lg">💡</span> Alasan Kenapa Jurusan Ini Cocok:
</h4>
<div class="space-y-2 sm:space-y-3">
<div class="flex gap-2 sm:gap-3">
<span class="text-lg flex-shrink-0">📊</span>
<p class="text-xs sm:text-sm text-gray-800">
<strong>Nilai Akademik:</strong> {{ $topRecommendation['explanation']['nilai'] ?? '-' }}
</p>
</div>
<div class="flex gap-2 sm:gap-3">
<span class="text-lg flex-shrink-0">❤️</span>
<p class="text-xs sm:text-sm text-gray-800">
<strong>Minat & Bakat:</strong> {{ $topRecommendation['explanation']['minat'] ?? '-' }}
</p>
</div>
<div class="flex gap-2 sm:gap-3">
<span class="text-lg flex-shrink-0">🎓</span>
<p class="text-xs sm:text-sm text-gray-800">
<strong>Metode Pembelajaran:</strong> {{ $topRecommendation['explanation']['pref'] ?? '-' }}
</p>
</div>
<div class="flex gap-2 sm:gap-3">
<span class="text-lg flex-shrink-0">🎯</span>
<p class="text-xs sm:text-sm text-gray-800">
<strong>Cita-cita Karir:</strong> {{ $topRecommendation['explanation']['cita'] ?? '-' }}
</p>
</div>
<div class="flex gap-2 sm:gap-3">
<span class="text-lg flex-shrink-0">🏆</span>
<p class="text-xs sm:text-sm text-gray-800">
<strong>Prestasi Akademik:</strong> {{ $topRecommendation['explanation']['prestasi'] ?? '-' }}
</p>
</div>
</div>
</div>
<!-- Kesimpulan -->
<div class="p-3 sm:p-4 bg-yellow-50 rounded-lg border-l-4 border-yellow-400 mb-3 sm:mb-4">
<h4 class="font-bold text-maroon mb-2 text-sm sm:text-base">📋 Kesimpulan:</h4>
<p class="text-gray-700 text-xs sm:text-sm leading-relaxed">
Berdasarkan profil Anda dengan <strong>nilai akademik {{ $katNilai }} (rata-rata {{ number_format($average, 1) }})</strong>
dan <strong>preferensi studi {{ $prefStudi ?? '-' }}</strong>,
sistem menganalisis bahwa <strong>{{ $topRecommendation['jurusan'] ?? '-' }}</strong>
adalah pilihan yang paling sesuai dengan skor {{ number_format(($topRecommendation['skor'] ?? 0) * 100, 1) }}%.
</p>
</div>
<!-- Prospek Kerja -->
@if($topJurusan && $topJurusan->prospek_kerja)
<div class="mb-3 sm:mb-4">
<p class="text-xs sm:text-sm font-bold text-maroon mb-2">Prospek Kerja:</p>
<p class="text-xs sm:text-sm text-gray-700">{{ $topJurusan->prospek_kerja }}</p>
</div>
@endif
</div>
@endif
<!-- Rekomendasi Alternatif & Penjelasan Detail -->
@if(count($hasilAkhir) > 1)
<div class="bg-white rounded-lg shadow-lg p-5 sm:p-8 mb-6 sm:mb-8">
<h3 class="text-lg sm:text-xl font-bold text-maroon mb-4 sm:mb-6 flex items-center gap-2">
<span class="text-2xl">🔍</span> Rekomendasi Alternatif & Penjelasan Detail
</h3>
<div class="space-y-3 sm:space-y-4">
@foreach($hasilAkhir as $index => $rec)
@if($index > 0)
<details class="border border-gray-300 rounded-lg overflow-hidden">
<summary class="cursor-pointer p-4 hover:bg-gray-50 flex justify-between items-center">
<div class="flex items-center gap-3">
<span class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-gray-200 text-gray-700 font-bold text-sm">
{{ $index + 1 }}
</span>
<div>
<p class="font-semibold text-gray-900 text-sm sm:text-base">{{ $rec['jurusan'] ?? '-' }}</p>
<p class="text-xs sm:text-sm text-gray-600">Skor: {{ number_format(($rec['skor'] ?? 0) * 100, 1) }}%</p>
</div>
</div>
<span class="text-gray-400"></span>
</summary>
<div class="p-4 bg-gray-50 border-t border-gray-300 space-y-4">
<!-- Score Breakdown -->
<div class="bg-white p-3 rounded-lg">
<p class="text-xs sm:text-sm font-bold text-gray-700 mb-3">Scoring per Kriteria:</p>
<div class="space-y-2">
<div class="flex justify-between items-center text-xs">
<span class="text-gray-600">📊 Nilai (15.6%)</span>
<span class="font-semibold text-maroon">{{ number_format(($rec['detail']['nilai'] ?? 0) * 100, 1) }}%</span>
</div>
<div class="w-full bg-gray-200 rounded h-1.5">
<div class="bg-red-500 rounded h-1.5 transition-all" style="width: {{ number_format(($rec['detail']['nilai'] ?? 0) * 100, 1) }}%"></div>
</div>
<div class="flex justify-between items-center text-xs mt-2">
<span class="text-gray-600">❤️ Minat (45.6%)</span>
<span class="font-semibold text-maroon">{{ number_format(($rec['detail']['minat'] ?? 0) * 100, 1) }}%</span>
</div>
<div class="w-full bg-gray-200 rounded h-1.5">
<div class="bg-pink-500 rounded h-1.5 transition-all" style="width: {{ number_format(($rec['detail']['minat'] ?? 0) * 100, 1) }}%"></div>
</div>
<div class="flex justify-between items-center text-xs mt-2">
<span class="text-gray-600">🎓 Preferensi (25.6%)</span>
<span class="font-semibold text-maroon">{{ number_format(($rec['detail']['pref'] ?? 0) * 100, 1) }}%</span>
</div>
<div class="w-full bg-gray-200 rounded h-1.5">
<div class="bg-yellow-500 rounded h-1.5 transition-all" style="width: {{ number_format(($rec['detail']['pref'] ?? 0) * 100, 1) }}%"></div>
</div>
<div class="flex justify-between items-center text-xs mt-2">
<span class="text-gray-600">🎯 Cita-cita (5%)</span>
<span class="font-semibold text-maroon">
@if(!($isPrestasiFilled ?? true))
Tidak dihitung
@else
{{ number_format(($rec['detail']['cita'] ?? 0) * 100, 1) }}%
@endif
</span>
</div>
<div class="w-full bg-gray-200 rounded h-1.5">
<div class="bg-blue-500 rounded h-1.5 transition-all" style="width: {{ number_format(($rec['detail']['cita'] ?? 0) * 100, 1) }}%"></div>
</div>
<div class="flex justify-between items-center text-xs mt-2">
<span class="text-gray-600">🏆 Prestasi ({{ ($isPrestasiFilled ?? true) ? '5%' : '0%' }})</span>
<span class="font-semibold text-maroon">
@if(!($isPrestasiFilled ?? true))
Tidak dihitung
@else
{{ number_format(($rec['detail']['prestasi'] ?? 0) * 100, 1) }}%
@endif
</span>
</div>
<div class="w-full bg-gray-200 rounded h-1.5">
<div class="bg-green-500 rounded h-1.5 transition-all" style="width: {{ number_format(($rec['detail']['prestasi'] ?? 0) * 100, 1) }}%"></div>
</div>
</div>
</div>
<!-- Explanation -->
@if(isset($rec['explanation']) && is_array($rec['explanation']))
<div class="bg-blue-50 p-3 rounded-lg border-l-4 border-blue-400">
<p class="text-xs sm:text-sm font-bold text-blue-900 mb-2">💡 Alasan Cocok:</p>
<ul class="space-y-1.5 text-xs sm:text-sm text-gray-800">
<li class="flex gap-2">
<span class="flex-shrink-0">📊</span>
<span>{{ $rec['explanation']['nilai'] ?? '-' }}</span>
</li>
<li class="flex gap-2">
<span class="flex-shrink-0">❤️</span>
<span>{{ $rec['explanation']['minat'] ?? '-' }}</span>
</li>
<li class="flex gap-2">
<span class="flex-shrink-0">🎓</span>
<span>{{ $rec['explanation']['pref'] ?? '-' }}</span>
</li>
<li class="flex gap-2">
<span class="flex-shrink-0">🎯</span>
<span>{{ $rec['explanation']['cita'] ?? '-' }}</span>
</li>
<li class="flex gap-2">
<span class="flex-shrink-0">🏆</span>
<span>{{ $rec['explanation']['prestasi'] ?? '-' }}</span>
</li>
</ul>
</div>
@endif
</div>
</details>
@endif
@endforeach
</div>
</div>
@endif
<!-- Chatbot Confirmation Card -->
<div class="bg-white rounded-lg shadow-lg p-5 sm:p-8 mb-6 sm:mb-8 border-2 border-yellow-400">
<div class="flex flex-col sm:flex-row items-start gap-3 sm:gap-4">
<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">💬</div>
<div class="flex-1">
<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">
Ingin tahu mengapa jurusan <strong>{{ $hasilAkhir[0]['jurusan'] ?? '' }}</strong> direkomendasikan?
Konsultasikan dengan AI Konselor BK Virtual untuk penjelasan detail berdasarkan profil Anda.
</p>
<a href="{{ route('chatbot.index', ['rec' => $recommendationId]) }}" 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">
💬 Tanya AI: "Mengapa jurusan ini cocok untukku?"
</a>
</div>
</div>
</div>
<!-- Tombol Aksi -->
<div class="flex flex-col sm:flex-row gap-3 sm:gap-4 justify-between">
<a href="{{ route('rekomendasi.index') }}" class="block sm:inline-flex items-center justify-center px-4 sm:px-6 py-2 sm:py-3 bg-gray-600 hover:bg-gray-700 text-white font-semibold rounded-lg transition duration-200 text-sm sm:text-base text-center">
Kembali & Ubah Input
</a>
<a href="{{ url('/dashboard') }}" class="block sm:inline-flex items-center justify-center px-4 sm:px-6 py-2 sm:py-3 gradient-maroon text-white font-semibold rounded-lg hover:opacity-90 transition duration-200 text-sm sm:text-base text-center">
Ke Dashboard
</a>
</div>
<!-- Info Metode -->
<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">
<strong>Metode:</strong> Sistem menggunakan algoritma Weighted Naive Bayes dengan 5 fitur berbobot: Nilai Akademik (w=0.156), Minat (w=0.456), Preferensi Studi (w=0.256), Cita-cita (w=0.090), Prestasi (w=0.040). Jika prestasi tidak diisi, atribut prestasi tidak dihitung (w=0.00) dan bobot atribut lain dinormalisasi. Rumus: P(H|X) &prop; P(H) &times; &prod; P(Xi|H)<sup>wi</sup>, kemudian dinormalisasi menggunakan softmax.
<!-- Method Explanation -->
<div class="card p-6 md:p-8 mt-8 bg-gradient-to-r from-cyan-50 to-blue-50">
<h3 class="text-lg font-bold text-primary mb-4 flex items-center gap-2">
<span>🔬</span> Metode Analisis
</h3>
<p style="font-size: 0.875rem; color: var(--text-secondary); line-height: 1.6;">
Sistem menggunakan <strong>Weighted Naive Bayes</strong> dengan 5 kriteria yang dibobotkan berdasarkan data historis siswa:
<br><strong>Minat (45.6%)</strong> Preferensi Studi (25.6%) Nilai Akademik (15.6%) Cita-cita (9%) Prestasi (4%)
<br>Jika prestasi tidak diisi, bobot distribusi ulang ke 4 kriteria lainnya untuk hasil yang akurat.
</p>
</div>
</div>
<!-- Action Buttons -->
<div class="flex flex-col sm:flex-row gap-4 mt-8">
<a href="{{ route('rekomendasi.index') }}" class="btn-primary px-6 py-3 rounded-lg font-semibold text-center hover:shadow-lg transition">
🔄 Analisis Ulang
</a>
<a href="{{ url('/dashboard') }}" class="px-6 py-3 rounded-lg font-semibold text-center bg-white text-primary border-2" style="border-color: var(--primary); transition: all 0.3s ease;" onmouseover="this.style.backgroundColor='var(--bg-soft)'" onmouseout="this.style.backgroundColor='white'">
Ke Dashboard
</a>
</div>
</main>
<script>
// Smooth animations for progress bars
window.addEventListener('load', () => {
document.querySelectorAll('.progress-fill').forEach(el => {
el.style.width = '0%';
setTimeout(() => {
el.style.width = el.parentElement.style.width;
}, 100);
});
});
</script>
</body>
</html>

View File

@ -6,129 +6,227 @@
<title>Input Data - Sistem Pemilihan Jurusan</title>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<style>
.gradient-maroon {
background: linear-gradient(135deg, #6B7280 0%, #8B95A5 100%);
* {
box-sizing: border-box;
}
.text-maroon {
color: #6B7280;
:root {
--primary: #0f766e;
--primary-light: #0d9488;
--secondary: #d97706;
--accent: #f59e0b;
--success: #10b981;
--error: #ef4444;
--bg-soft: #f0f9ff;
--bg-card: #ffffff;
--text-main: #1f2937;
--text-secondary: #6b7280;
--border-light: #e5e7eb;
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.08);
}
.border-maroon {
border-color: #6B7280;
body {
background-color: var(--bg-soft);
color: var(--text-main);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
.bg-cream {
background-color: #F8FAFC;
.btn-primary {
background-color: var(--primary);
color: white;
transition: all 0.3s ease;
}
.focus-maroon:focus {
border-color: #6B7280;
box-shadow: 0 0 0 3px rgba(107, 114, 128, 0.1);
.btn-primary:hover {
background-color: var(--primary-light);
box-shadow: var(--shadow-lg);
}
.btn-primary:active {
transform: scale(0.98);
}
.text-primary {
color: var(--primary);
}
.border-primary {
border-color: var(--primary);
}
.focus-primary:focus {
border-color: var(--primary);
outline: none;
box-shadow: 0 0 0 3px rgba(15, 118, 110, 0.1);
}
.input-error {
border-color: #ef4444 !important;
background-color: #fef2f2 !important;
border-color: var(--error) !important;
background-color: #fff5f5 !important;
}
.input-valid {
border-color: #10b981 !important;
border-color: var(--success) !important;
background-color: #f0fdf4 !important;
}
.error-icon::before {
content: "⚠️ ";
margin-right: 0.25rem;
}
.success-icon::before {
content: "";
margin-right: 0.25rem;
}
.input-wrapper {
position: relative;
}
.validation-message {
font-size: 0.75rem;
margin-top: 0.25rem;
margin-top: 0.375rem;
display: flex;
align-items: center;
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-5px);
transform: translateY(-4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.card {
background: var(--bg-card);
border-radius: 12px;
box-shadow: var(--shadow-md);
border: 1px solid var(--border-light);
}
.card-header {
display: flex;
align-items: flex-start;
gap: 1rem;
margin-bottom: 1.5rem;
}
.card-icon {
width: 3rem;
height: 3rem;
border-radius: 8px;
background: linear-gradient(135deg, rgba(15, 118, 110, 0.1) 0%, rgba(13, 148, 136, 0.1) 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
flex-shrink: 0;
}
.header-main {
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-light) 100%);
color: white;
position: sticky;
top: 0;
z-index: 50;
box-shadow: var(--shadow-md);
}
.section-title {
font-size: 1.125rem;
font-weight: 600;
color: var(--primary);
margin-bottom: 0.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.section-desc {
font-size: 0.875rem;
color: var(--text-secondary);
margin-bottom: 1rem;
}
.input-field {
display: flex;
flex-direction: column;
}
.input-field label {
font-size: 0.875rem;
font-weight: 500;
color: var(--text-main);
margin-bottom: 0.375rem;
}
.input-field input,
.input-field select,
.input-field textarea {
padding: 0.625rem 0.875rem;
border: 1px solid var(--border-light);
border-radius: 8px;
font-size: 0.875rem;
transition: all 0.2s ease;
font-family: inherit;
}
.input-field input:focus,
.input-field select:focus,
.input-field textarea:focus {
border-color: var(--primary);
outline: none;
box-shadow: 0 0 0 3px rgba(15, 118, 110, 0.08);
background-color: #f8feff;
}
.input-hint {
font-size: 0.75rem;
color: var(--text-secondary);
margin-top: 0.375rem;
}
</style>
</head>
<body class="bg-cream">
<body>
<!-- Header -->
<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>
<h1 class="text-xl sm:text-2xl md:text-3xl font-bold">Input Data Rekomendasi</h1>
<p class="text-xs sm:text-sm text-yellow-300 font-semibold mt-1">Sistem Pemilihan Jurusan</p>
</div>
<div class="flex items-center gap-2 sm:gap-4 w-full sm:w-auto">
<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 ke Dashboard
<header class="header-main py-4 md:py-6">
<div class="container mx-auto px-4 md:px-6">
<div class="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
<div>
<h1 class="text-2xl md:text-3xl font-bold">Form Analisis Jurusan</h1>
<p class="text-sm md:text-base text-teal-100 mt-1">Temukan jurusan yang paling sesuai dengan minat dan potensi Anda</p>
</div>
<a href="{{ url('/dashboard') }}" class="btn-primary px-4 py-2 rounded-lg font-medium text-sm md:text-base hover:shadow-lg transition">
Kembali
</a>
</div>
</div>
</header>
<!-- Main Content -->
<div class="w-full px-4 sm:px-6 py-6 sm:py-8">
<!-- Hero Intro -->
<div class="bg-gradient-to-r from-blue-600 to-purple-600 rounded-xl shadow-lg p-8 sm:p-10 mb-8 text-white">
<h2 class="text-3xl sm:text-4xl font-bold mb-3 sm:mb-4">Kuis Cerdas Menemukan Jurusanmu 🚀</h2>
<p class="text-base sm:text-lg text-blue-100 mb-4 sm:mb-6 leading-relaxed">
Kami akan mengajukan beberapa pertanyaan untuk mengenal lebih dalam tentang profil akademis, minat, gaya belajar, prestasi, dan impian karirmu. Jawab dengan jujur dan sedetail mungkin - informasi ini akan membantu sistem AI kami memberikan rekomendasi yang paling akurat untuk masa depan gemilangmu.
</p>
<div class="grid grid-cols-3 gap-4 mt-6">
<div class="bg-white bg-opacity-20 rounded-lg p-4 backdrop-blur-sm text-center">
<p class="text-3xl font-bold">⏱️</p>
<p class="text-sm text-blue-100 mt-1">3-5 Menit</p>
</div>
<div class="bg-white bg-opacity-20 rounded-lg p-4 backdrop-blur-sm text-center">
<p class="text-3xl font-bold"></p>
<p class="text-sm text-blue-100 mt-1">Mudah & Cepat</p>
</div>
<div class="bg-white bg-opacity-20 rounded-lg p-4 backdrop-blur-sm text-center">
<p class="text-3xl font-bold">🎯</p>
<p class="text-sm text-blue-100 mt-1">Hasil Akurat</p>
</div>
</div>
</div>
<!-- Tips Card -->
<div class="bg-amber-50 border-l-4 border-amber-400 rounded-lg p-6 sm:p-8 mb-8 shadow-sm">
<div class="flex gap-4">
<div class="text-3xl flex-shrink-0">💡</div>
<main class="container mx-auto px-4 md:px-6 py-8 md:py-12">
<!-- Info Banner -->
<div class="card p-5 md:p-6 mb-6 md:mb-8 bg-gradient-to-r from-blue-50 to-cyan-50 border-l-4" style="border-left-color: var(--primary);">
<div style="display: flex; gap: 1rem;">
<div style="font-size: 2rem; flex-shrink: 0;">🎯</div>
<div>
<h3 class="font-bold text-amber-900 mb-3 text-lg">Tips Agar Hasil Lebih Akurat</h3>
<ul class="text-sm text-amber-800 space-y-2">
<li> Jawab semua pertanyaan dengan jujur dan sebenar-benarnya</li>
<li> Jangan terburu-buru - pikirkan jawaban dengan matang</li>
<li> Nilai yang kamu masukkan sebaiknya merupakan rata-rata atau nilai terbaik dari rapor</li>
<li> Pilih opsi yang benar-benar mewakili minat dan preferensi kamu</li>
</ul>
<h2 class="text-lg font-semibold mb-2 text-primary">Petunjuk Pengisian</h2>
<p class="text-sm mb-2">Isi form berikut dengan data yang akurat. Sistem akan menganalisis profil Anda menggunakan algoritma Weighted Naive Bayes untuk merekomendasikan 9 jurusan terbaik.</p>
<p class="text-xs" style="color: var(--text-secondary);">Bobot kriteria: Minat (45.6%) Preferensi (25.6%) Nilai (15.6%) Cita-cita (9%) Prestasi (4%)</p>
</div>
</div>
</div>
<!-- Form Card -->
<div class="bg-white rounded-xl shadow-lg p-8 sm:p-10 border-t-4 border-purple-600">
<div class="card p-6 md:p-8">
<h2 class="text-2xl font-bold mb-6 text-primary">Data Diri Siswa</h2>
@if ($errors->any())
<div class="bg-red-50 border-l-4 border-red-500 p-4 sm:p-5 rounded-lg mb-6 shadow-md animate-pulse">
<div class="flex items-start gap-3">
<span class="text-2xl flex-shrink-0"></span>
<div class="flex-1">
<h3 class="text-red-700 font-bold text-sm sm:text-base mb-3">Terjadi Kesalahan Validasi</h3>
<p class="text-red-600 text-xs sm:text-sm mb-3">Silakan perbaiki kesalahan berikut sebelum melanjutkan:</p>
<ul class="list-disc list-inside space-y-2 text-red-600 text-xs sm:text-sm bg-white bg-opacity-50 p-3 rounded">
<div class="mb-6 p-4 rounded-lg bg-red-50 border-l-4" style="border-left-color: var(--error);">
<div style="display: flex; gap: 1rem;">
<div style="font-size: 1.5rem; flex-shrink: 0;"></div>
<div>
<h3 style="color: var(--error); font-weight: 600; margin-bottom: 0.5rem;">Ada Kesalahan Pengisian</h3>
<ul style="list-style: none; padding: 0; margin: 0;">
@foreach ($errors->all() as $error)
<li class="ml-2">{{ $error }}</li>
<li style="font-size: 0.875rem; color: var(--error); margin-bottom: 0.375rem;">
{{ $error }}
</li>
@endforeach
</ul>
</div>
@ -136,182 +234,136 @@
</div>
@endif
<form action="{{ route('rekomendasi.proses') }}" method="POST" class="space-y-4 sm:space-y-6">
<form action="{{ route('rekomendasi.proses') }}" method="POST" class="space-y-8">
@csrf
{{-- ============================================ --}}
{{-- KRITERIA 1: NILAI MATA PELAJARAN --}}
{{-- ============================================ --}}
<div class="p-6 rounded-lg border-2 border-gray-200 bg-gray-50">
<h3 class="font-bold text-lg sm:text-xl text-purple-700 mb-2">1️⃣ Nilai Mata Pelajaran <span class="text-red-500">*</span></h3>
<p class="text-sm text-gray-600 mb-4">
<!-- NILAI AKADEMIK -->
<section>
<h3 class="section-title">
<span style="font-size: 1.25rem;">1️⃣</span> Nilai Akademik
<span style="color: var(--error);">*</span>
</h3>
<p class="section-desc">
@if(isset($student) && $student->kelompok_asal == 'IPA')
Siswa <strong>IPA</strong> Masukkan nilai rapor (0-100): <strong>Matematika, Fisika, Kimia, Biologi</strong>.
Masukkan nilai rapor untuk IPA: <strong>Matematika, Fisika, Kimia, Biologi</strong>
@else
Siswa <strong>IPS</strong> Masukkan nilai rapor (0-100): <strong>Ekonomi, Geografi, Sosiologi, Sejarah</strong>.
Masukkan nilai rapor untuk IPS: <strong>Ekonomi, Geografi, Sosiologi, Sejarah</strong>
@endif
</p>
@if(isset($student) && $student->kelompok_asal == 'IPA')
{{-- SISWA IPA: Matematika, Fisika, Kimia, Biologi --}}
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<div class="input-wrapper">
<label for="mtk" class="block text-sm font-semibold text-gray-700 mb-2">Matematika <span class="text-red-500">*</span></label>
<input id="mtk" type="number" name="mtk" min="0" max="100" value="{{ old('mtk') }}" placeholder="Nilai: 85" required
class="block w-full px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm transition-colors @error('mtk') input-error @enderror">
@error('mtk')
<span class="validation-message text-red-600">⚠️ {{ $message }}</span>
@enderror
</div>
<div class="input-wrapper">
<label for="fisika" class="block text-sm font-semibold text-gray-700 mb-2">Fisika <span class="text-red-500">*</span></label>
<input id="fisika" type="number" name="fisika" min="0" max="100" value="{{ old('fisika') }}" placeholder="Nilai: 78" required
class="block w-full px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm transition-colors @error('fisika') input-error @enderror">
@error('fisika')
<span class="validation-message text-red-600">⚠️ {{ $message }}</span>
@enderror
</div>
<div class="input-wrapper">
<label for="kimia" class="block text-sm font-semibold text-gray-700 mb-2">Kimia <span class="text-red-500">*</span></label>
<input id="kimia" type="number" name="kimia" min="0" max="100" value="{{ old('kimia') }}" placeholder="Nilai: 72" required
class="block w-full px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm transition-colors @error('kimia') input-error @enderror">
@error('kimia')
<span class="validation-message text-red-600">⚠️ {{ $message }}</span>
@enderror
</div>
<div class="input-wrapper">
<label for="biologi" class="block text-sm font-semibold text-gray-700 mb-2">Biologi <span class="text-red-500">*</span></label>
<input id="biologi" type="number" name="biologi" min="0" max="100" value="{{ old('biologi') }}" placeholder="Nilai: 80" required
class="block w-full px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm transition-colors @error('biologi') input-error @enderror">
@error('biologi')
<span class="validation-message text-red-600">⚠️ {{ $message }}</span>
@enderror
</div>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
@foreach(['mtk' => 'Matematika', 'fisika' => 'Fisika', 'kimia' => 'Kimia', 'biologi' => 'Biologi'] as $field => $label)
<div class="input-field">
<label>{{ $label }}</label>
<input type="number" name="{{ $field }}" min="0" max="100" value="{{ old($field) }}" placeholder="0-100" required
class="focus-primary @error($field) input-error @enderror">
@error($field)
<span class="validation-message" style="color: var(--error);">⚠️ {{ $message }}</span>
@enderror
</div>
@endforeach
</div>
@else
{{-- SISWA IPS: Ekonomi, Geografi, Sosiologi, Sejarah --}}
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<div class="input-wrapper">
<label for="ekonomi" class="block text-sm font-semibold text-gray-700 mb-2">Ekonomi <span class="text-red-500">*</span></label>
<input id="ekonomi" type="number" name="ekonomi" min="0" max="100" value="{{ old('ekonomi') }}" placeholder="Nilai: 82" required
class="block w-full px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm transition-colors @error('ekonomi') input-error @enderror">
@error('ekonomi')
<span class="validation-message text-red-600">⚠️ {{ $message }}</span>
@enderror
</div>
<div class="input-wrapper">
<label for="geografi" class="block text-sm font-semibold text-gray-700 mb-2">Geografi <span class="text-red-500">*</span></label>
<input id="geografi" type="number" name="geografi" min="0" max="100" value="{{ old('geografi') }}" placeholder="Nilai: 76" required
class="block w-full px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm transition-colors @error('geografi') input-error @enderror">
@error('geografi')
<span class="validation-message text-red-600">⚠️ {{ $message }}</span>
@enderror
</div>
<div class="input-wrapper">
<label for="sosiologi" class="block text-sm font-semibold text-gray-700 mb-2">Sosiologi <span class="text-red-500">*</span></label>
<input id="sosiologi" type="number" name="sosiologi" min="0" max="100" value="{{ old('sosiologi') }}" placeholder="Nilai: 74" required
class="block w-full px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm transition-colors @error('sosiologi') input-error @enderror">
@error('sosiologi')
<span class="validation-message text-red-600">⚠️ {{ $message }}</span>
@enderror
</div>
<div class="input-wrapper">
<label for="sejarah" class="block text-sm font-semibold text-gray-700 mb-2">Sejarah <span class="text-red-500">*</span></label>
<input id="sejarah" type="number" name="sejarah" min="0" max="100" value="{{ old('sejarah') }}" placeholder="Nilai: 70" required
class="block w-full px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm transition-colors @error('sejarah') input-error @enderror">
@error('sejarah')
<span class="validation-message text-red-600">⚠️ {{ $message }}</span>
@enderror
</div>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
@foreach(['ekonomi' => 'Ekonomi', 'geografi' => 'Geografi', 'sosiologi' => 'Sosiologi', 'sejarah' => 'Sejarah'] as $field => $label)
<div class="input-field">
<label>{{ $label }}</label>
<input type="number" name="{{ $field }}" min="0" max="100" value="{{ old($field) }}" placeholder="0-100" required
class="focus-primary @error($field) input-error @enderror">
@error($field)
<span class="validation-message" style="color: var(--error);">⚠️ {{ $message }}</span>
@enderror
</div>
@endforeach
</div>
@endif
</div>
</section>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
{{-- ============================================ --}}
{{-- KRITERIA 2: MINAT SISWA --}}
{{-- ============================================ --}}
<div class="p-6 rounded-lg border-2 border-gray-200 bg-gray-50">
<h3 class="font-bold text-lg sm:text-xl text-blue-700 mb-2">2️⃣ Minat Siswa <span class="text-red-500">*</span></h3>
<p class="text-sm text-gray-600 mb-4">Tuliskan bidang atau kegiatan yang Anda minati / sukai.</p>
<div class="input-wrapper">
<label for="minat" class="block text-sm font-semibold text-gray-700 mb-2">Bidang Minat</label>
<input id="minat" type="text" name="minat" value="{{ old('minat') }}" placeholder="Contoh: coding, komputer, bisnis, pertanian"
class="block w-full px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm transition-colors @error('minat') input-error @enderror" required>
<p class="text-xs text-gray-500 mt-2">Pisahkan dengan koma jika lebih dari satu minat</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<!-- MINAT -->
<section>
<h3 class="section-title">
<span style="font-size: 1.25rem;">2️⃣</span> Bidang Minat
<span style="color: var(--error);">*</span>
</h3>
<p class="section-desc">Tuliskan bidang atau kegiatan yang Anda minati. Contoh: coding, pertanian, seni, dll.</p>
<div class="input-field">
<input type="text" name="minat" value="{{ old('minat') }}" placeholder="Minat Anda..." required
class="focus-primary @error('minat') input-error @enderror">
<span class="input-hint">Minimal 3 karakter, pisahkan dengan koma jika lebih dari satu</span>
@error('minat')
<span class="validation-message text-red-600">⚠️ {{ $message }}</span>
<span class="validation-message" style="color: var(--error);">⚠️ {{ $message }}</span>
@enderror
</div>
</div>
</section>
{{-- ============================================ --}}
{{-- KRITERIA 3: PREFERENSI STUDI LANJUTAN --}}
{{-- ============================================ --}}
<div class="p-6 rounded-lg border-2 border-gray-200 bg-gray-50">
<h3 class="font-bold text-lg sm:text-xl text-green-700 mb-2">3️⃣ Preferensi Studi Lanjutan <span class="text-red-500">*</span></h3>
<p class="text-sm text-gray-600 mb-4">Pilih rumpun jurusan Politeknik Negeri Jember yang paling sesuai.</p>
<div class="input-wrapper">
<label for="pref_studi" class="block text-sm font-semibold text-gray-700 mb-2">Arah Rumpun Jurusan Tujuan</label>
<select id="pref_studi" name="pref_studi" class="block w-full px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm transition-colors @error('pref_studi') input-error @enderror" required>
<option value="">-- Pilih Arah Rumpun Jurusan --</option>
<option value="Sains & Teknologi" {{ old('pref_studi') == 'Sains & Teknologi' ? 'selected' : '' }}>Sains & Teknologi (contoh: TI, Teknik)</option>
<option value="Pertanian & Lingkungan" {{ old('pref_studi') == 'Pertanian & Lingkungan' ? 'selected' : '' }}>Pertanian & Lingkungan (contoh: Produksi/Teknologi Pertanian)</option>
<option value="Kesehatan & Ilmu Hayat" {{ old('pref_studi') == 'Kesehatan & Ilmu Hayat' ? 'selected' : '' }}>Kesehatan & Ilmu Hayat (contoh: rumpun Kesehatan)</option>
<option value="Bisnis & Manajemen" {{ old('pref_studi') == 'Bisnis & Manajemen' ? 'selected' : '' }}>Bisnis & Manajemen (contoh: Akuntansi, Manajemen Agribisnis)</option>
<option value="Sosial & Humaniora" {{ old('pref_studi') == 'Sosial & Humaniora' ? 'selected' : '' }}>Sosial & Humaniora (contoh: Bahasa, Komunikasi, Pariwisata)</option>
<!-- PREFERENSI STUDI -->
<section>
<h3 class="section-title">
<span style="font-size: 1.25rem;">3️⃣</span> Preferensi Studi
<span style="color: var(--error);">*</span>
</h3>
<p class="section-desc">Pilih rumpun jurusan yang paling Anda minati untuk studi lanjutan.</p>
<div class="input-field">
<select name="pref_studi" required class="focus-primary @error('pref_studi') input-error @enderror">
<option value="">-- Pilih Preferensi --</option>
<option value="Sains & Teknologi" {{ old('pref_studi') == 'Sains & Teknologi' ? 'selected' : '' }}>Sains & Teknologi</option>
<option value="Pertanian & Lingkungan" {{ old('pref_studi') == 'Pertanian & Lingkungan' ? 'selected' : '' }}>Pertanian & Lingkungan</option>
<option value="Kesehatan & Ilmu Hayat" {{ old('pref_studi') == 'Kesehatan & Ilmu Hayat' ? 'selected' : '' }}>Kesehatan & Ilmu Hayat</option>
<option value="Bisnis & Manajemen" {{ old('pref_studi') == 'Bisnis & Manajemen' ? 'selected' : '' }}>Bisnis & Manajemen</option>
<option value="Sosial & Humaniora" {{ old('pref_studi') == 'Sosial & Humaniora' ? 'selected' : '' }}>Sosial & Humaniora</option>
</select>
<p class="text-xs text-gray-500 mt-2">Pilih rumpun yang paling menggambarkan jurusan Polije yang ingin Anda tuju.</p>
@error('pref_studi')
<span class="validation-message text-red-600">⚠️ {{ $message }}</span>
<span class="validation-message" style="color: var(--error);">⚠️ {{ $message }}</span>
@enderror
</div>
</div>
{{-- ============================================ --}}
{{-- KRITERIA 4: CITA-CITA / PREFERENSI KARIR --}}
{{-- ============================================ --}}
<div class="p-6 rounded-lg border-2 border-gray-200 bg-gray-50">
<h3 class="font-bold text-lg sm:text-xl text-orange-700 mb-2">4️⃣ Cita-cita / Preferensi Karir <span class="text-red-500">*</span></h3>
<p class="text-sm text-gray-600 mb-4">Tuliskan profesi atau karir yang Anda impikan.</p>
<div class="input-wrapper">
<label for="cita_cita" class="block text-sm font-semibold text-gray-700 mb-2">Cita-cita</label>
<input id="cita_cita" type="text" name="cita_cita" value="{{ old('cita_cita') }}" placeholder="Contoh: programmer, dokter, pengusaha"
class="block w-full px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm transition-colors @error('cita_cita') input-error @enderror" required>
<p class="text-xs text-gray-500 mt-2">Bisa lebih dari satu, pisahkan dengan koma</p>
@error('cita_cita')
<span class="validation-message text-red-600">⚠️ {{ $message }}</span>
@enderror
</div>
</div>
{{-- ============================================ --}}
{{-- KRITERIA 5: PRESTASI AKADEMIK / NON-AKADEMIK --}}
{{-- ============================================ --}}
<div class="p-6 rounded-lg border-2 border-gray-200 bg-gray-50">
<h3 class="font-bold text-lg sm:text-xl text-red-700 mb-2">5️⃣ Prestasi (Opsional)</h3>
<p class="text-sm text-gray-600 mb-4">Tuliskan prestasi yang pernah diraih (opsional).</p>
<div class="input-wrapper">
<label for="prestasi" class="block text-sm font-semibold text-gray-700 mb-2">Prestasi</label>
<input id="prestasi" type="text" name="prestasi" value="{{ old('prestasi') }}" placeholder="Contoh: Juara 1 olimpiade MTK, sertifikat web design"
class="block w-full px-4 py-2 border border-gray-300 rounded-lg focus-maroon focus:outline-none text-sm transition-colors @error('prestasi') input-error @enderror">
<p class="text-xs text-gray-500 mt-2">Kosongkan jika belum ada prestasi</p>
@error('prestasi')
<span class="validation-message text-red-600">⚠️ {{ $message }}</span>
@enderror
</div>
</div>
</section>
</div>
<!-- Submit Button -->
<div class="mt-8 p-6 rounded-lg bg-gradient-to-r from-yellow-50 to-yellow-100 border-l-4 border-yellow-400 shadow-sm">
<p class="text-sm text-gray-700 mb-4 leading-relaxed">
Setelah menekan tombol, sistem akan menganalisis data Anda dengan algoritma Naive Bayes dan menampilkan ranking 9 jurusan Politeknik Negeri Jember yang paling sesuai dengan profil kamu.
</p>
<button type="submit" class="w-full bg-gradient-to-r from-purple-600 to-indigo-600 text-white font-bold py-3 px-6 rounded-lg hover:shadow-lg active:scale-95 transition duration-200 text-base shadow-lg disabled:opacity-50 disabled:cursor-not-allowed">
Lihat Rekomendasi Jurusan
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<!-- CITA-CITA -->
<section>
<h3 class="section-title">
<span style="font-size: 1.25rem;">4️⃣</span> Cita-cita / Karir Impian
<span style="color: var(--error);">*</span>
</h3>
<p class="section-desc">Tuliskan profesi atau karir yang Anda impikan di masa depan.</p>
<div class="input-field">
<input type="text" name="cita_cita" value="{{ old('cita_cita') }}" placeholder="Contoh: programmer, dokter, pengusaha..." required
class="focus-primary @error('cita_cita') input-error @enderror">
<span class="input-hint">Minimal 3 karakter, pisahkan dengan koma jika lebih dari satu</span>
@error('cita_cita')
<span class="validation-message" style="color: var(--error);">⚠️ {{ $message }}</span>
@enderror
</div>
</section>
<!-- PRESTASI -->
<section>
<h3 class="section-title">
<span style="font-size: 1.25rem;">5️⃣</span> Prestasi (Opsional)
</h3>
<p class="section-desc">Tuliskan prestasi akademik atau non-akademik yang pernah Anda raih.</p>
<div class="input-field">
<input type="text" name="prestasi" value="{{ old('prestasi') }}" placeholder="Contoh: Juara olimpiade, sertifikat programming..."
class="focus-primary @error('prestasi') input-error @enderror">
<span class="input-hint">Kolom ini bersifat opsional</span>
@error('prestasi')
<span class="validation-message" style="color: var(--error);">⚠️ {{ $message }}</span>
@enderror
</div>
</section>
</div>
<!-- SUBMIT BUTTON -->
<div class="pt-4">
<button type="submit" class="btn-primary w-full py-3 rounded-lg font-semibold text-white text-base md:text-lg shadow-md hover:shadow-lg transition">
Analisis & Lihat Rekomendasi
</button>
<p class="text-xs sm:text-sm text-gray-600 mt-3 text-center">Pastikan semua data terisi dengan benar sebelum melanjutkan</p>
<p style="font-size: 0.875rem; color: var(--text-secondary); margin-top: 1rem; text-align: center;">
Proses analisis membutuhkan waktu 1-2 detik
</p>
</div>
</form>
</div>
@ -360,47 +412,24 @@ class="block w-full px-4 py-2 border border-gray-300 rounded-lg focus-maroon foc
</div>
@endif
<!-- Info Metode -->
<div class="mt-8 sm:mt-12 p-6 rounded-lg bg-blue-50 border-l-4 border-blue-400 shadow-sm">
<div class="flex gap-4">
<div class="text-3xl flex-shrink-0">🤖</div>
<div>
<h3 class="font-bold text-blue-900 mb-2 text-lg">Tentang Sistem Rekomendasi Kami</h3>
<p class="text-sm text-blue-800 leading-relaxed">
<strong>Metode:</strong> Sistem menggunakan Weighted Naive Bayes dengan 5 kriteria:
</p>
<ul class="text-sm text-blue-800 mt-3 space-y-1 ml-4">
<li>📚 Nilai Akademik (15.6%)</li>
<li>💡 Minat & Bakat (45.6%)</li>
<li>🎯 Preferensi Studi (25.6%)</li>
<li>🚀 Cita-cita (9%)</li>
<li>🏆 Prestasi (4%)</li>
</ul>
</div>
</div>
<!-- Footer Info -->
<div class="card p-4 md:p-5 mt-8 bg-gradient-to-r from-teal-50 to-cyan-50">
<p style="font-size: 0.875rem; color: var(--text-secondary);">
<strong>📊 Metode Analisis:</strong> Weighted Naive Bayes dengan 5 kriteria yang diseimbangkan berdasarkan data historis siswa Polije.
</p>
</div>
</div>
</main>
<script>
document.addEventListener('DOMContentLoaded', function() {
const form = document.querySelector('form');
const submitBtn = form?.querySelector('button[type="submit"]');
// Validasi input fields
const inputs = form?.querySelectorAll('input, select, textarea');
inputs?.forEach(input => {
// Validasi pada change event
input.addEventListener('change', function() {
validateField(this);
});
// Validasi pada blur event
input.addEventListener('blur', function() {
validateField(this);
});
// Remove error on input
input.addEventListener('change', () => validateField(input));
input.addEventListener('blur', () => validateField(input));
input.addEventListener('input', function() {
if (this.classList.contains('input-error')) {
validateField(this);
@ -408,10 +437,8 @@ class="block w-full px-4 py-2 border border-gray-300 rounded-lg focus-maroon foc
});
});
// Validasi saat submit
form?.addEventListener('submit', function(e) {
let isValid = true;
inputs?.forEach(input => {
if (!validateField(input)) {
isValid = false;
@ -420,7 +447,6 @@ class="block w-full px-4 py-2 border border-gray-300 rounded-lg focus-maroon foc
if (!isValid) {
e.preventDefault();
// Scroll ke error pertama
const firstError = form.querySelector('.input-error');
if (firstError) {
firstError.scrollIntoView({ behavior: 'smooth', block: 'center' });
@ -432,74 +458,40 @@ class="block w-full px-4 py-2 border border-gray-300 rounded-lg focus-maroon foc
function validateField(field) {
const value = field.value.trim();
const isRequired = field.hasAttribute('required');
const name = field.name;
const type = field.type;
let errorMsg = '';
let isValid = true;
// Remove previous error/valid styling
field.classList.remove('input-error', 'input-valid');
const existingMsg = field.parentElement.querySelector('.validation-message');
if (existingMsg) {
existingMsg.remove();
}
if (existingMsg) existingMsg.remove();
// Validasi untuk field yang required
if (isRequired && !value) {
errorMsg = '⚠️ ' + field.placeholder?.split('Contoh')[0]?.trim() + ' tidak boleh kosong';
isValid = false;
}
// Validasi untuk number fields
if (type === 'number' && value) {
const numValue = parseFloat(value);
const min = parseFloat(field.min || 0);
const max = parseFloat(field.max || 100);
if (isNaN(numValue)) {
errorMsg = '⚠️ Masukkan angka yang valid (0-100)';
isValid = false;
} else if (numValue < min || numValue > max) {
errorMsg = '⚠️ Nilai harus antara ' + min + ' - ' + max;
} else if (type === 'number' && value) {
const num = parseFloat(value);
if (isNaN(num) || num < 0 || num > 100) {
isValid = false;
}
}
// Validasi untuk text fields (min length)
if ((name === 'minat' || name === 'cita_cita') && value && value.length < 3) {
errorMsg = '⚠️ Minimal 3 karakter, jelaskan lebih detail';
} else if ((field.name === 'minat' || field.name === 'cita_cita') && value && value.length < 3) {
isValid = false;
}
// Validasi untuk select (pref_studi)
if (name === 'pref_studi' && !value) {
errorMsg = '⚠️ Pilih salah satu preferensi studi';
isValid = false;
}
// Apply styling
if (!isValid && value) {
if (!isValid && (value || isRequired)) {
field.classList.add('input-error');
} else if (isValid && (isRequired || value)) {
field.classList.add('input-valid');
}
// Show error message
if (errorMsg) {
const msgDiv = document.createElement('div');
msgDiv.className = 'validation-message text-red-600';
msgDiv.textContent = errorMsg;
field.parentElement.appendChild(msgDiv);
}
return isValid || !isRequired;
}
// Loading state pada submit
form?.addEventListener('submit', function() {
submitBtn.disabled = true;
submitBtn.innerHTML = '<span class="inline-flex items-center">⏳ Menganalisis data...</span>';
if (submitBtn) {
submitBtn.disabled = true;
submitBtn.innerHTML = '⏳ Menganalisis...';
}
});
});
</script>
</body>
</body>
</html>

View File

@ -82,6 +82,9 @@
Route::get('/profil', [AdminController::class, 'profil'])->name('profil');
Route::put('/profil', [AdminController::class, 'updateProfil'])->name('profil.update');
Route::put('/profil/password', [AdminController::class, 'updatePassword'])->name('profil.password');
// 8. Logout Semua User
Route::post('/logout-all-users', [AdminController::class, 'logoutAllUsers'])->name('logout-all-users');
});
// BK Routes (role-based access control)
@ -113,4 +116,9 @@
Route::put('/profil/password', [BKController::class, 'updatePassword'])->name('profil.password');
});
require __DIR__.'/auth.php';
require __DIR__.'/auth.php';
// Password reset with 6-digit code (inline token flow)
use App\Http\Controllers\Auth\PasswordResetWithCodeController;
Route::post('/password/reset-with-code', [PasswordResetWithCodeController::class, 'resetWithCode'])->name('password.reset.with_code');
Route::post('/password/verify-code', [PasswordResetWithCodeController::class, 'verifyCode'])->name('password.verify.code');