Add 6-digit reset code flow: migration, notification, controller, routes, and forgot-password UI
This commit is contained in:
parent
24a7387cfb
commit
2905df7309
|
|
@ -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!');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.']);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.']);
|
||||
}
|
||||
}
|
||||
|
|
@ -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'
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@
|
|||
'users' => [
|
||||
'provider' => 'users',
|
||||
'table' => 'password_reset_tokens',
|
||||
'expire' => 60,
|
||||
'expire' => 1440, // 24 hours (was 60 minutes)
|
||||
'throttle' => 60,
|
||||
],
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
};
|
||||
|
|
@ -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',
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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.'],
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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')"
|
||||
|
|
|
|||
|
|
@ -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) × 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) × 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) × 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) × 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) × 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) ∝ P(H) × ∏ 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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');
|
||||
Loading…
Reference in New Issue