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')
|
->groupBy('kelompok_asal')
|
||||||
->get();
|
->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("
|
$topMajors = Recommendation::selectRaw("
|
||||||
JSON_EXTRACT(hasil_rekomendasi, '$[0].jurusan') as major_name,
|
JSON_EXTRACT(hasil_rekomendasi, '$[0].jurusan') as major_name,
|
||||||
COUNT(*) as count
|
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")
|
||||||
->orderBy('count', 'desc')
|
->whereRaw("JSON_EXTRACT(hasil_rekomendasi, '\$[0].jurusan') != 'null'") ->orderBy('count', 'desc')
|
||||||
->take(5)
|
->take(5)
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
// Data untuk chart - semua jurusan
|
// Data untuk chart - semua jurusan (filter out NULL values)
|
||||||
$allMajorsChart = Recommendation::selectRaw("
|
$allMajorsChart = Recommendation::selectRaw("
|
||||||
JSON_EXTRACT(hasil_rekomendasi, '$[0].jurusan') as major_name,
|
JSON_EXTRACT(hasil_rekomendasi, '\$[0].jurusan') as major_name,
|
||||||
COUNT(*) as count
|
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')
|
->orderBy('count', 'desc')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
// Persiapkan data untuk Chart.js
|
// Persiapkan data untuk Chart.js - aggregate & fix major names
|
||||||
$chartMajorNames = $allMajorsChart->pluck('major_name')->map(function($name) {
|
$majorData = [];
|
||||||
return trim($name, '"');
|
foreach ($allMajorsChart as $item) {
|
||||||
})->toArray();
|
$name = trim($item->major_name, '" ');
|
||||||
$chartMajorCounts = $allMajorsChart->pluck('count')->toArray();
|
|
||||||
|
// 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();
|
$chartKelompokNames = $kelompokStats->pluck('kelompok_asal')->toArray();
|
||||||
$chartKelompokCounts = $kelompokStats->pluck('count')->toArray();
|
$chartKelompokCounts = $kelompokStats->pluck('count')->toArray();
|
||||||
|
|
||||||
// Top majors untuk horizontal bar chart
|
// Top majors untuk horizontal bar chart - aggregate & fix
|
||||||
$topMajorsChart = $topMajors->pluck('major_name')->map(function($name) {
|
$topMajorData = [];
|
||||||
return trim($name, '"');
|
foreach ($topMajors as $item) {
|
||||||
})->toArray();
|
$name = trim($item->major_name, '" ');
|
||||||
$topMajorsCounts = $topMajors->pluck('count')->toArray();
|
|
||||||
|
// 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(
|
return view('admin.dashboard', compact(
|
||||||
'totalSiswa',
|
'totalSiswa',
|
||||||
|
|
@ -85,7 +137,8 @@ public function dashboard()
|
||||||
'chartKelompokNames',
|
'chartKelompokNames',
|
||||||
'chartKelompokCounts',
|
'chartKelompokCounts',
|
||||||
'topMajorsChart',
|
'topMajorsChart',
|
||||||
'topMajorsCounts'
|
'topMajorsCounts',
|
||||||
|
'rekomendasiPerKelompok'
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -460,4 +513,17 @@ public function updatePassword(Request $request)
|
||||||
return redirect()->route('admin.profil')->with('success', 'Password berhasil diubah!');
|
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
|
// 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.
|
// redirect them back to where they came from with their error message.
|
||||||
return $status == Password::PASSWORD_RESET
|
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'))
|
: 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')
|
->groupBy('kelompok_asal')
|
||||||
->get();
|
->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("
|
$topMajors = Recommendation::selectRaw("
|
||||||
JSON_EXTRACT(hasil_rekomendasi, '$[0].jurusan') as major_name,
|
JSON_EXTRACT(hasil_rekomendasi, '$[0].jurusan') as major_name,
|
||||||
COUNT(*) as count
|
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")
|
||||||
->orderBy('count', 'desc')
|
->whereRaw("JSON_EXTRACT(hasil_rekomendasi, '\$[0].jurusan') != 'null'") ->orderBy('count', 'desc')
|
||||||
->take(5)
|
->take(5)
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
// Data untuk chart - semua jurusan
|
// Data untuk chart - semua jurusan (filter out NULL values)
|
||||||
$allMajorsChart = Recommendation::selectRaw("
|
$allMajorsChart = Recommendation::selectRaw("
|
||||||
JSON_EXTRACT(hasil_rekomendasi, '$[0].jurusan') as major_name,
|
JSON_EXTRACT(hasil_rekomendasi, '\$[0].jurusan') as major_name,
|
||||||
COUNT(*) as count
|
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')
|
->orderBy('count', 'desc')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
// Persiapkan data untuk Chart.js
|
// Persiapkan data untuk Chart.js - aggregate & fix major names
|
||||||
$chartMajorNames = $allMajorsChart->pluck('major_name')->map(function($name) {
|
$majorData = [];
|
||||||
return trim($name, '"');
|
foreach ($allMajorsChart as $item) {
|
||||||
})->toArray();
|
$name = trim($item->major_name, '" ');
|
||||||
$chartMajorCounts = $allMajorsChart->pluck('count')->toArray();
|
|
||||||
|
// 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();
|
$chartKelompokNames = $kelompokStats->pluck('kelompok_asal')->toArray();
|
||||||
$chartKelompokCounts = $kelompokStats->pluck('count')->toArray();
|
$chartKelompokCounts = $kelompokStats->pluck('count')->toArray();
|
||||||
|
|
||||||
// Top majors untuk horizontal bar chart
|
// Top majors untuk horizontal bar chart - aggregate & fix
|
||||||
$topMajorsChart = $topMajors->pluck('major_name')->map(function($name) {
|
$topMajorData = [];
|
||||||
return trim($name, '"');
|
foreach ($topMajors as $item) {
|
||||||
})->toArray();
|
$name = trim($item->major_name, '" ');
|
||||||
$topMajorsCounts = $topMajors->pluck('count')->toArray();
|
|
||||||
|
// 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(
|
return view('bk.dashboard', compact(
|
||||||
'totalSiswa',
|
'totalSiswa',
|
||||||
|
|
@ -117,7 +169,8 @@ public function dashboard()
|
||||||
'chartKelompokNames',
|
'chartKelompokNames',
|
||||||
'chartKelompokCounts',
|
'chartKelompokCounts',
|
||||||
'topMajorsChart',
|
'topMajorsChart',
|
||||||
'topMajorsCounts'
|
'topMajorsCounts',
|
||||||
|
'rekomendasiPerKelompok'
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -255,27 +255,25 @@ public function proses(Request $request)
|
||||||
$prior = 1 / $cfgCount;
|
$prior = 1 / $cfgCount;
|
||||||
$logPrior = log(max($prior, $epsilon));
|
$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%)
|
// Use global ROC weights (override any per-jurusan editable weights)
|
||||||
$weights = $c['weights'] ?? ['nilai' => 0.156, 'minat' => 0.456, 'pref' => 0.256, 'cita_cita' => 0.090, 'prestasi' => 0.040];
|
// 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;
|
||||||
|
|
||||||
// Ensure weights is array
|
// Jika prestasi kosong, atribut prestasi tidak dihitung dan lakukan normalisasi ulang pada atribut lain
|
||||||
if (!is_array($weights)) {
|
|
||||||
$weights = ['nilai' => 0.156, 'minat' => 0.456, 'pref' => 0.256, 'cita_cita' => 0.090, 'prestasi' => 0.040];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Jika prestasi kosong, atribut prestasi tidak dihitung dengan normalisasi ulang
|
|
||||||
if (!$isPrestasiFilled) {
|
if (!$isPrestasiFilled) {
|
||||||
$weights['prestasi'] = 0.0;
|
$weights['prestasi'] = 0.0;
|
||||||
$sumNonPrestasi = ($weights['nilai'] ?? 0) + ($weights['minat'] ?? 0) + ($weights['cita_cita'] ?? 0);
|
$sumNonPrestasi = ($weights['nilai'] ?? 0) + ($weights['minat'] ?? 0) + ($weights['pref'] ?? 0) + ($weights['cita_cita'] ?? 0);
|
||||||
|
|
||||||
// Normalize weights dengan safety check
|
// Normalize weights dengan safety check
|
||||||
if ($sumNonPrestasi > $epsilon) {
|
if ($sumNonPrestasi > $epsilon) {
|
||||||
$weights['nilai'] = ($weights['nilai'] ?? 0) / $sumNonPrestasi;
|
$weights['nilai'] = ($weights['nilai'] ?? 0) / $sumNonPrestasi;
|
||||||
$weights['minat'] = ($weights['minat'] ?? 0) / $sumNonPrestasi;
|
$weights['minat'] = ($weights['minat'] ?? 0) / $sumNonPrestasi;
|
||||||
|
$weights['pref'] = ($weights['pref'] ?? 0) / $sumNonPrestasi;
|
||||||
$weights['cita_cita'] = ($weights['cita_cita'] ?? 0) / $sumNonPrestasi;
|
$weights['cita_cita'] = ($weights['cita_cita'] ?? 0) / $sumNonPrestasi;
|
||||||
} else {
|
} else {
|
||||||
// Fallback weights jika semua weight adalah 0
|
// Fallback ke global jika normalisasi gagal
|
||||||
$weights = ['nilai' => 0.156, 'minat' => 0.456, 'pref' => 0.256, 'cita_cita' => 0.090, 'prestasi' => 0.040];
|
$weights = $globalWeights;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,11 +46,20 @@ public function toMail(object $notifiable): MailMessage
|
||||||
'email' => $notifiable->getEmailForPasswordReset(),
|
'email' => $notifiable->getEmailForPasswordReset(),
|
||||||
], false));
|
], 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)
|
return (new MailMessage)
|
||||||
->subject('🔑 Reset Password - Sistem Pemilihan Jurusan Polije')
|
->subject('🔑 Reset Password - Sistem Pemilihan Jurusan Polije')
|
||||||
->view('emails.reset-password', [
|
->view('emails.reset-password', [
|
||||||
'user' => $notifiable,
|
'user' => $notifiable,
|
||||||
'resetUrl' => $resetUrl,
|
'resetUrl' => $resetUrl,
|
||||||
|
'code' => $code,
|
||||||
|
'token' => $this->token,
|
||||||
'expiresIn' => config('auth.passwords.users.expire', 60),
|
'expiresIn' => config('auth.passwords.users.expire', 60),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@
|
||||||
'users' => [
|
'users' => [
|
||||||
'provider' => 'users',
|
'provider' => 'users',
|
||||||
'table' => 'password_reset_tokens',
|
'table' => 'password_reset_tokens',
|
||||||
'expire' => 60,
|
'expire' => 1440, // 24 hours (was 60 minutes)
|
||||||
'throttle' => 60,
|
'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',
|
'cita_cita' => 'Software Developer',
|
||||||
'preferensi_studi' => 'Sains & Teknologi',
|
'preferensi_studi' => 'Sains & Teknologi',
|
||||||
'prestasi' => 'Juara 1 Olimpiade Komputer Nasional',
|
'prestasi' => 'Juara 1 Olimpiade Komputer Nasional',
|
||||||
'major_masuk' => 'Teknik Informatika',
|
'major_masuk' => 'Teknologi Informasi',
|
||||||
'tahun_lulus_polije' => 2027,
|
'tahun_lulus_polije' => 2027,
|
||||||
'catatan' => 'Alumni 2023 - Rekomendasi akurat',
|
'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, [
|
Recommendation::create(array_merge($data, [
|
||||||
'hasil_rekomendasi' => [
|
'hasil_rekomendasi' => [
|
||||||
[
|
[
|
||||||
'jurusan' => 'Teknik Informatika',
|
'jurusan' => 'Teknologi Informasi',
|
||||||
'skor' => rand(70, 95) / 100,
|
'skor' => rand(70, 95) / 100,
|
||||||
'detail' => [
|
'detail' => [
|
||||||
'nilai' => rand(60, 95) / 100,
|
'nilai' => rand(60, 95) / 100,
|
||||||
|
|
@ -274,7 +274,7 @@ public function run(): void
|
||||||
// Data chat history yang natural dan tidak template
|
// Data chat history yang natural dan tidak template
|
||||||
$chatData = [
|
$chatData = [
|
||||||
// Chat tentang rekomendasi
|
// 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' => 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' => '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.'],
|
['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.'],
|
['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
|
// 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' => 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' => 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.'],
|
['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">
|
<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.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>
|
<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')
|
@csrf @method('DELETE')
|
||||||
<button type="submit" class="text-red-600 hover:text-red-800 font-semibold text-sm">🗑 Hapus</button>
|
<button type="submit" class="text-red-600 hover:text-red-800 font-semibold text-sm">🗑 Hapus</button>
|
||||||
</form>
|
</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">
|
<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
|
✏ Edit
|
||||||
</a>
|
</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')
|
@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">
|
<button type="submit" class="px-6 py-2 rounded-lg font-bold bg-red-500 text-white hover:bg-red-600 transition">
|
||||||
🗑 Hapus
|
🗑 Hapus
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,13 @@
|
||||||
@section('title', 'Dashboard')
|
@section('title', 'Dashboard')
|
||||||
|
|
||||||
@section('content')
|
@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 -->
|
<!-- Statistics Cards -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
<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">
|
<div class="stat-card bg-white rounded-lg shadow p-6 border-t-4 border-maroon">
|
||||||
|
|
@ -42,13 +49,13 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Kelompok Distribution -->
|
<!-- Rekomendasi & Top Majors -->
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||||
<!-- Kelompok Chart -->
|
<!-- Rekomendasi per Kelompok -->
|
||||||
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-maroon">
|
<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>
|
<h3 class="text-lg font-bold text-maroon mb-4">📊 Rekomendasi per Kelompok</h3>
|
||||||
<div style="position: relative; height: 250px;">
|
<div style="position: relative; height: 250px;">
|
||||||
<canvas id="chartKelompokPie"></canvas>
|
<canvas id="chartRekomendasiKelompok"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -67,7 +74,7 @@
|
||||||
@if($recentStudents->isNotEmpty())
|
@if($recentStudents->isNotEmpty())
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="w-full text-sm">
|
<table class="w-full text-sm">
|
||||||
<thead class="border-b-2 border-maroon">
|
<thead class="border-b-2 border-purple-500">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="text-left px-4 py-2 font-bold text-maroon">Nama</th>
|
<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>
|
<th class="text-center px-4 py-2 font-bold text-maroon">NIS</th>
|
||||||
|
|
@ -86,7 +93,7 @@
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-2 text-center">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@endforeach
|
@endforeach
|
||||||
|
|
@ -207,17 +214,18 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Chart 3: Kelompok Pie Chart
|
// Chart 3: Rekomendasi per Kelompok Bar Chart
|
||||||
const chartKelompokPieCtx = document.getElementById('chartKelompokPie').getContext('2d');
|
const chartRekomendasiKelompokCtx = document.getElementById('chartRekomendasiKelompok').getContext('2d');
|
||||||
const chartKelompokPie = new Chart(chartKelompokPieCtx, {
|
const chartRekomendasiKelompok = new Chart(chartRekomendasiKelompokCtx, {
|
||||||
type: 'pie',
|
type: 'bar',
|
||||||
data: {
|
data: {
|
||||||
labels: @json($chartKelompokNames),
|
labels: @json($rekomendasiPerKelompok->pluck('kelompok_asal')->toArray()),
|
||||||
datasets: [{
|
datasets: [{
|
||||||
data: @json($chartKelompokCounts),
|
label: 'Jumlah Rekomendasi',
|
||||||
|
data: @json($rekomendasiPerKelompok->pluck('count')->toArray()),
|
||||||
backgroundColor: ['#0369A1', '#D97706'],
|
backgroundColor: ['#0369A1', '#D97706'],
|
||||||
borderColor: '#fff',
|
borderColor: ['#0369A1', '#D97706'],
|
||||||
borderWidth: 2
|
borderWidth: 1
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
|
|
@ -225,10 +233,17 @@
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: {
|
legend: {
|
||||||
position: 'bottom',
|
display: true,
|
||||||
labels: {
|
labels: {
|
||||||
font: { size: 11 },
|
font: { size: 11 }
|
||||||
padding: 10
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
ticks: {
|
||||||
|
font: { size: 10 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -272,4 +287,34 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</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
|
@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">
|
<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
|
Edit
|
||||||
</a>
|
</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
|
@csrf
|
||||||
@method('DELETE')
|
@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">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Bobot Mata Pelajaran -->
|
<!-- ROC Weights Info (Fixed) -->
|
||||||
<div class="bg-white rounded-lg shadow p-6">
|
<div class="bg-white rounded-lg shadow p-6">
|
||||||
<h3 class="text-lg font-bold text-maroon mb-4">⚖️ Bobot Mata Pelajaran</h3>
|
<h3 class="text-lg font-bold text-maroon mb-4">⚖️ Bobot Kriteria Penilaian (ROC-Validated)</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>
|
<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 class="grid grid-cols-2 md:grid-cols-5 gap-3">
|
||||||
<div>
|
<div class="p-3 bg-green-50 rounded-lg text-center border-l-4 border-green-400">
|
||||||
<h4 class="text-sm font-bold text-gray-700 mb-3 border-b pb-2">📐 IPA</h4>
|
<p class="text-xs font-bold text-green-800">💡 Minat</p>
|
||||||
<div class="space-y-3">
|
<p class="text-lg font-bold text-green-700">45.6%</p>
|
||||||
<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>
|
</div>
|
||||||
<div>
|
<div class="p-3 bg-yellow-50 rounded-lg text-center border-l-4 border-yellow-400">
|
||||||
<h4 class="text-sm font-bold text-gray-700 mb-3 border-b pb-2">📊 IPS</h4>
|
<p class="text-xs font-bold text-yellow-800">🎯 Preferensi</p>
|
||||||
<div class="space-y-3">
|
<p class="text-lg font-bold text-yellow-700">25.6%</p>
|
||||||
<div>
|
</div>
|
||||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Ekonomi</label>
|
<div class="p-3 bg-blue-50 rounded-lg text-center border-l-4 border-blue-400">
|
||||||
<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">
|
<p class="text-xs font-bold text-blue-800">📝 Nilai</p>
|
||||||
</div>
|
<p class="text-lg font-bold text-blue-700">15.6%</p>
|
||||||
<div>
|
</div>
|
||||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Geografi</label>
|
<div class="p-3 bg-red-50 rounded-lg text-center border-l-4 border-red-400">
|
||||||
<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">
|
<p class="text-xs font-bold text-red-800">💼 Cita-cita</p>
|
||||||
</div>
|
<p class="text-lg font-bold text-red-700">9.0%</p>
|
||||||
<div>
|
</div>
|
||||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Sosiologi</label>
|
<div class="p-3 bg-purple-50 rounded-lg text-center border-l-4 border-purple-400">
|
||||||
<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">
|
<p class="text-xs font-bold text-purple-800">🏆 Prestasi</p>
|
||||||
</div>
|
<p class="text-lg font-bold text-purple-700">4.0%</p>
|
||||||
<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>
|
</div>
|
||||||
</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>
|
||||||
|
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
|
|
|
||||||
|
|
@ -90,60 +90,40 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Bobot Mata Pelajaran -->
|
<!-- ROC Weights Info (Fixed) -->
|
||||||
@php
|
@php
|
||||||
$bobot = $jurusan->bobot_mapel ?? [];
|
$bobot = $jurusan->bobot_mapel ?? [];
|
||||||
$bobotIpa = data_get($bobot, 'ipa', $bobot);
|
$bobotIpa = data_get($bobot, 'ipa', $bobot);
|
||||||
$bobotIps = data_get($bobot, 'ips', $bobot);
|
$bobotIps = data_get($bobot, 'ips', $bobot);
|
||||||
@endphp
|
@endphp
|
||||||
<div class="bg-white rounded-lg shadow p-6">
|
<div class="bg-white rounded-lg shadow p-6">
|
||||||
<h3 class="text-lg font-bold text-maroon mb-4">⚖️ Bobot Mata Pelajaran</h3>
|
<h3 class="text-lg font-bold text-maroon mb-4">⚖️ Bobot Kriteria Penilaian (ROC-Validated)</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>
|
<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 class="grid grid-cols-2 md:grid-cols-5 gap-3">
|
||||||
<div>
|
<div class="p-3 bg-green-50 rounded-lg text-center border-l-4 border-green-400">
|
||||||
<h4 class="text-sm font-bold text-gray-700 mb-3 border-b pb-2">📐 IPA</h4>
|
<p class="text-xs font-bold text-green-800">💡 Minat</p>
|
||||||
<div class="space-y-3">
|
<p class="text-lg font-bold text-green-700">45.6%</p>
|
||||||
<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>
|
</div>
|
||||||
<div>
|
<div class="p-3 bg-yellow-50 rounded-lg text-center border-l-4 border-yellow-400">
|
||||||
<h4 class="text-sm font-bold text-gray-700 mb-3 border-b pb-2">📊 IPS</h4>
|
<p class="text-xs font-bold text-yellow-800">🎯 Preferensi</p>
|
||||||
<div class="space-y-3">
|
<p class="text-lg font-bold text-yellow-700">25.6%</p>
|
||||||
<div>
|
</div>
|
||||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Ekonomi</label>
|
<div class="p-3 bg-blue-50 rounded-lg text-center border-l-4 border-blue-400">
|
||||||
<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">
|
<p class="text-xs font-bold text-blue-800">📝 Nilai</p>
|
||||||
</div>
|
<p class="text-lg font-bold text-blue-700">15.6%</p>
|
||||||
<div>
|
</div>
|
||||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Geografi</label>
|
<div class="p-3 bg-red-50 rounded-lg text-center border-l-4 border-red-400">
|
||||||
<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">
|
<p class="text-xs font-bold text-red-800">💼 Cita-cita</p>
|
||||||
</div>
|
<p class="text-lg font-bold text-red-700">9.0%</p>
|
||||||
<div>
|
</div>
|
||||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Sosiologi</label>
|
<div class="p-3 bg-purple-50 rounded-lg text-center border-l-4 border-purple-400">
|
||||||
<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">
|
<p class="text-xs font-bold text-purple-800">🏆 Prestasi</p>
|
||||||
</div>
|
<p class="text-lg font-bold text-purple-700">4.0%</p>
|
||||||
<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>
|
</div>
|
||||||
</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>
|
||||||
|
|
||||||
<div class="flex gap-4">
|
<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">
|
<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
|
✏️ Edit
|
||||||
</a>
|
</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
|
@csrf
|
||||||
@method('DELETE')
|
@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">
|
<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>
|
<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">
|
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
.gradient-maroon { background: linear-gradient(135deg, #6B7280 0%, #8B95A5 100%); }
|
.gradient-maroon { background: linear-gradient(135deg, #7c3aed 0%, #6366f1 100%); }
|
||||||
.text-maroon { color: #6B7280; }
|
.text-maroon { color: #7c3aed; }
|
||||||
.border-maroon { border-color: #6B7280; }
|
.border-maroon { border-color: #7c3aed; }
|
||||||
.bg-cream { background-color: #F8FAFC; }
|
.bg-cream { background-color: #f8fafc; }
|
||||||
.bg-maroon { background-color: #6B7280; }
|
.bg-maroon { background-color: #7c3aed; }
|
||||||
.hover\:bg-maroon:hover { background-color: #8B95A5; }
|
.hover\:bg-maroon:hover { background-color: #6366f1; }
|
||||||
.stat-card { transition: all 0.3s ease; }
|
.stat-card { transition: all 0.3s ease; }
|
||||||
.stat-card:hover { transform: translateY(-5px); box-shadow: 0 5px 15px rgba(107, 114, 128, 0.1); }
|
.stat-card:hover { transform: translateY(-5px); box-shadow: 0 5px 15px rgba(124, 58, 237, 0.1); }
|
||||||
|
|
||||||
/* Sidebar */
|
/* Sidebar */
|
||||||
.sidebar-dark {
|
.sidebar-dark {
|
||||||
|
|
@ -27,14 +27,14 @@
|
||||||
color: #cbd5e1;
|
color: #cbd5e1;
|
||||||
}
|
}
|
||||||
.sidebar-link:hover {
|
.sidebar-link:hover {
|
||||||
background: rgba(107, 114, 128, 0.12);
|
background: rgba(124, 58, 237, 0.12);
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
border-left-color: rgba(107, 114, 128, 0.5);
|
border-left-color: rgba(124, 58, 237, 0.5);
|
||||||
}
|
}
|
||||||
.sidebar-link.active {
|
.sidebar-link.active {
|
||||||
background: linear-gradient(90deg, rgba(107,114,128,0.2) 0%, rgba(107,114,128,0.03) 100%);
|
background: linear-gradient(90deg, rgba(124, 58, 237, 0.2) 0%, rgba(124, 58, 237, 0.03) 100%);
|
||||||
color: #b0b9c8 !important;
|
color: #c4b5fd !important;
|
||||||
border-left-color: #b0b9c8;
|
border-left-color: #c4b5fd;
|
||||||
}
|
}
|
||||||
.sidebar-link .sidebar-icon {
|
.sidebar-link .sidebar-icon {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|
@ -49,11 +49,11 @@
|
||||||
transition: all 0.25s ease;
|
transition: all 0.25s ease;
|
||||||
}
|
}
|
||||||
.sidebar-link:hover .sidebar-icon {
|
.sidebar-link:hover .sidebar-icon {
|
||||||
background: rgba(107, 114, 128, 0.25);
|
background: rgba(124, 58, 237, 0.25);
|
||||||
transform: scale(1.05);
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
.sidebar-link.active .sidebar-icon {
|
.sidebar-link.active .sidebar-icon {
|
||||||
background: rgba(107, 114, 128, 0.15);
|
background: rgba(124, 58, 237, 0.15);
|
||||||
}
|
}
|
||||||
.sidebar-section-label {
|
.sidebar-section-label {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
|
|
@ -73,13 +73,13 @@
|
||||||
.sidebar-brand-icon {
|
.sidebar-brand-icon {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
background: linear-gradient(135deg, #6B7280 0%, #8B95A5 100%);
|
background: linear-gradient(135deg, #7c3aed 0%, #6366f1 100%);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 20px;
|
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 {
|
.sidebar-footer {
|
||||||
border-top: 1px solid rgba(255,255,255,0.06);
|
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">
|
<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
|
👤 Profil Admin
|
||||||
</a>
|
</a>
|
||||||
<form method="POST" action="{{ route('logout') }}">
|
<form method="POST" action="{{ route('logout') }}" class="confirm-logout">
|
||||||
@csrf
|
@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">
|
<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
|
🚪 Logout
|
||||||
|
|
@ -279,6 +279,50 @@
|
||||||
mobileOverlay.addEventListener('click', () => mobileSidebar.classList.add('hidden'));
|
mobileOverlay.addEventListener('click', () => mobileSidebar.classList.add('hidden'));
|
||||||
closeMobileMenu.addEventListener('click', () => mobileSidebar.classList.add('hidden'));
|
closeMobileMenu.addEventListener('click', () => mobileSidebar.classList.add('hidden'));
|
||||||
</script>
|
</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')
|
@yield('scripts')
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,64 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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
|
@endif
|
||||||
|
|
||||||
<!-- Error Messages -->
|
<!-- 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">
|
<div class="text-center mt-6 text-xs text-gray-500">
|
||||||
<p>💡 Jika email tidak masuk, cek folder spam kamu.</p>
|
<p>💡 Jika email tidak masuk, cek folder spam kamu.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- (Token panel removed) -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -134,6 +194,9 @@ function handleForgotSubmit(event) {
|
||||||
return false;
|
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
|
// Disable button dan tampilkan loading state
|
||||||
submitBtn.disabled = true;
|
submitBtn.disabled = true;
|
||||||
submitBtn.textContent = '⏳ Sedang Mengirim...';
|
submitBtn.textContent = '⏳ Sedang Mengirim...';
|
||||||
|
|
@ -142,5 +205,158 @@ function handleForgotSubmit(event) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
</script>
|
</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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -405,6 +405,20 @@
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@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') }}">
|
<form method="POST" action="{{ route('login') }}">
|
||||||
@csrf
|
@csrf
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -81,12 +81,25 @@
|
||||||
</li>
|
</li>
|
||||||
@endforeach
|
@endforeach
|
||||||
</ul>
|
</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">
|
@if($errors->has('token'))
|
||||||
<li>✓ Password minimal 8 karakter</li>
|
<div class="mt-4 pt-3 border-t border-red-200">
|
||||||
<li>✓ Kedua password sama persis</li>
|
<p class="text-red-700 text-xs font-semibold mb-2">⏰ Token telah kadaluarsa?</p>
|
||||||
<li>✓ Tidak menggunakan password lama</li>
|
<p class="text-red-700 text-xs mb-3">Minta tautan reset password yang baru:</p>
|
||||||
</ul>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@
|
||||||
<td class="px-6 py-4 text-center gap-2 flex justify-center">
|
<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.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>
|
<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')
|
@csrf @method('DELETE')
|
||||||
<button type="submit" class="text-red-600 hover:text-red-800 font-semibold text-sm">🗑 Hapus</button>
|
<button type="submit" class="text-red-600 hover:text-red-800 font-semibold text-sm">🗑 Hapus</button>
|
||||||
</form>
|
</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">
|
<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
|
✏ Edit
|
||||||
</a>
|
</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')
|
@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">
|
<button type="submit" class="px-6 py-2 rounded-lg font-bold bg-red-500 text-white hover:bg-red-600 transition">
|
||||||
🗑 Hapus
|
🗑 Hapus
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
|
|
||||||
<!-- Statistics Cards -->
|
<!-- Statistics Cards -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
<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-gray-600 text-sm font-semibold">👥 Total Siswa</p>
|
||||||
<p class="text-3xl font-bold text-bk mt-2">{{ $totalSiswa }}</p>
|
<p class="text-3xl font-bold text-bk mt-2">{{ $totalSiswa }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -47,13 +47,13 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Kelompok Distribution & Top Majors -->
|
<!-- Rekomendasi & Top Majors -->
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||||
<!-- Kelompok Chart -->
|
<!-- Rekomendasi per Kelompok -->
|
||||||
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-teal-500">
|
<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">📊 Siswa per Kelompok</h3>
|
<h3 class="text-lg font-bold text-bk mb-4">📊 Rekomendasi per Kelompok</h3>
|
||||||
<div style="position: relative; height: 250px;">
|
<div style="position: relative; height: 250px;">
|
||||||
<canvas id="chartKelompokPie"></canvas>
|
<canvas id="chartRekomendasiKelompok"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -72,7 +72,7 @@
|
||||||
@if($recentStudents->isNotEmpty())
|
@if($recentStudents->isNotEmpty())
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="w-full text-sm">
|
<table class="w-full text-sm">
|
||||||
<thead class="border-b-2 border-teal-500">
|
<thead class="border-b-2 border-purple-500">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="text-left px-4 py-2 font-bold text-bk">Nama</th>
|
<th class="text-left px-4 py-2 font-bold text-bk">Nama</th>
|
||||||
<th class="text-center px-4 py-2 font-bold text-bk">NIS</th>
|
<th class="text-center px-4 py-2 font-bold text-bk">NIS</th>
|
||||||
|
|
@ -91,7 +91,7 @@
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-2 text-center">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@endforeach
|
@endforeach
|
||||||
|
|
@ -212,17 +212,18 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Chart 3: Kelompok Pie Chart
|
// Chart 3: Rekomendasi per Kelompok Bar Chart
|
||||||
const chartKelompokPieCtx = document.getElementById('chartKelompokPie').getContext('2d');
|
const chartRekomendasiKelompokCtx = document.getElementById('chartRekomendasiKelompok').getContext('2d');
|
||||||
const chartKelompokPie = new Chart(chartKelompokPieCtx, {
|
const chartRekomendasiKelompok = new Chart(chartRekomendasiKelompokCtx, {
|
||||||
type: 'pie',
|
type: 'bar',
|
||||||
data: {
|
data: {
|
||||||
labels: @json($chartKelompokNames),
|
labels: @json($rekomendasiPerKelompok->pluck('kelompok_asal')->toArray()),
|
||||||
datasets: [{
|
datasets: [{
|
||||||
data: @json($chartKelompokCounts),
|
label: 'Jumlah Rekomendasi',
|
||||||
|
data: @json($rekomendasiPerKelompok->pluck('count')->toArray()),
|
||||||
backgroundColor: ['#0369A1', '#D97706'],
|
backgroundColor: ['#0369A1', '#D97706'],
|
||||||
borderColor: '#fff',
|
borderColor: ['#0369A1', '#D97706'],
|
||||||
borderWidth: 2
|
borderWidth: 1
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
|
|
@ -230,10 +231,17 @@
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: {
|
legend: {
|
||||||
position: 'bottom',
|
display: true,
|
||||||
labels: {
|
labels: {
|
||||||
font: { size: 11 },
|
font: { size: 11 }
|
||||||
padding: 10
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
ticks: {
|
||||||
|
font: { size: 10 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,58 +72,18 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Bobot Mata Pelajaran -->
|
<!-- Bobot Mata Pelajaran dihapus: gunakan bobot ROC global -->
|
||||||
<div class="bg-white rounded-lg shadow p-6">
|
<div class="bg-white rounded-lg shadow p-6">
|
||||||
<h3 class="text-lg font-bold text-bk mb-4">⚖️ Bobot Mata Pelajaran</h3>
|
<h3 class="text-lg font-bold text-bk mb-4">⚖️ Bobot Penilaian (ROC)</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>
|
<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">
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<li><strong>Minat</strong>: 45.6% (0.456)</li>
|
||||||
<div>
|
<li><strong>Preferensi</strong>: 25.6% (0.256)</li>
|
||||||
<h4 class="text-sm font-bold text-gray-700 mb-3 border-b pb-2">📐 IPA</h4>
|
<li><strong>Nilai</strong>: 15.6% (0.156)</li>
|
||||||
<div class="space-y-3">
|
<li><strong>Cita-cita</strong>: 9.0% (0.090)</li>
|
||||||
<div>
|
<li><strong>Prestasi</strong>: 4.0% (0.040)</li>
|
||||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Matematika</label>
|
</ul>
|
||||||
<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()">
|
<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>
|
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
|
|
@ -172,32 +132,8 @@ function validateJurusanForm() {
|
||||||
submitButton.disabled = !isNamaValid;
|
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() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
validateJurusanForm();
|
validateJurusanForm();
|
||||||
validateBobot();
|
|
||||||
updateCharCount('deskripsi');
|
updateCharCount('deskripsi');
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div>
|
<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>
|
<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">
|
<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="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>
|
<span id="namaValid" class="text-green-500 text-xs hidden">✓ Nama valid</span>
|
||||||
|
|
@ -80,63 +80,18 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Bobot Mata Pelajaran -->
|
<!-- Bobot Mata Pelajaran dihapus: gunakan bobot ROC global -->
|
||||||
@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">
|
<div class="bg-white rounded-lg shadow p-6">
|
||||||
<h3 class="text-lg font-bold text-bk mb-4">⚖️ Bobot Mata Pelajaran</h3>
|
<h3 class="text-lg font-bold text-bk mb-4">⚖️ Bobot Penilaian (ROC)</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>
|
<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">
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<li><strong>Minat</strong>: 45.6% (0.456)</li>
|
||||||
<div>
|
<li><strong>Preferensi</strong>: 25.6% (0.256)</li>
|
||||||
<h4 class="text-sm font-bold text-gray-700 mb-3 border-b pb-2">📐 IPA</h4>
|
<li><strong>Nilai</strong>: 15.6% (0.156)</li>
|
||||||
<div class="space-y-3">
|
<li><strong>Cita-cita</strong>: 9.0% (0.090)</li>
|
||||||
<div>
|
<li><strong>Prestasi</strong>: 4.0% (0.040)</li>
|
||||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Matematika</label>
|
</ul>
|
||||||
<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()">
|
<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>
|
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
|
|
@ -185,32 +140,8 @@ function validateJurusanForm() {
|
||||||
submitButton.disabled = !isNamaValid;
|
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() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
validateJurusanForm();
|
validateJurusanForm();
|
||||||
validateBobot();
|
|
||||||
updateCharCount('deskripsi');
|
updateCharCount('deskripsi');
|
||||||
});
|
});
|
||||||
</script>
|
</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">
|
<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
|
✏️ Edit
|
||||||
</a>
|
</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
|
@csrf
|
||||||
@method('DELETE')
|
@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">
|
<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>
|
<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">
|
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
/* Keep BK theme, but also support admin-style utility classes used by Alumni pages */
|
/* Updated BK theme to match modern purple-indigo gradient */
|
||||||
.gradient-maroon { background: linear-gradient(135deg, #6B7280 0%, #8B95A5 100%); }
|
.gradient-maroon { background: linear-gradient(135deg, #7c3aed 0%, #6366f1 100%); }
|
||||||
.text-maroon { color: #6B7280; }
|
.text-maroon { color: #7c3aed; }
|
||||||
.border-maroon { border-color: #6B7280; }
|
.border-maroon { border-color: #7c3aed; }
|
||||||
.bg-maroon { background-color: #6B7280; }
|
.bg-maroon { background-color: #7c3aed; }
|
||||||
|
|
||||||
.gradient-bk { background: linear-gradient(135deg, #5A8A7F 0%, #7BA39A 100%); }
|
.gradient-bk { background: linear-gradient(135deg, #7c3aed 0%, #6366f1 100%); }
|
||||||
.text-bk { color: #5A8A7F; }
|
.text-bk { color: #7c3aed; }
|
||||||
.border-bk { border-color: #5A8A7F; }
|
.border-bk { border-color: #7c3aed; }
|
||||||
.bg-cream { background-color: #F8FAFC; }
|
.bg-cream { background-color: #f8fafc; }
|
||||||
.stat-card { transition: all 0.3s ease; }
|
.stat-card { transition: all 0.3s ease; }
|
||||||
.stat-card:hover { transform: translateY(-5px); box-shadow: 0 5px 15px rgba(90, 138, 127, 0.1); }
|
.stat-card:hover { transform: translateY(-5px); box-shadow: 0 5px 15px rgba(124, 58, 237, 0.1); }
|
||||||
|
|
||||||
/* Sidebar */
|
/* Sidebar */
|
||||||
.sidebar-dark {
|
.sidebar-dark {
|
||||||
|
|
@ -31,14 +31,14 @@
|
||||||
color: #cbd5e1;
|
color: #cbd5e1;
|
||||||
}
|
}
|
||||||
.sidebar-link:hover {
|
.sidebar-link:hover {
|
||||||
background: rgba(90, 138, 127, 0.12);
|
background: rgba(124, 58, 237, 0.12);
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
border-left-color: rgba(90, 138, 127, 0.5);
|
border-left-color: rgba(124, 58, 237, 0.5);
|
||||||
}
|
}
|
||||||
.sidebar-link.active {
|
.sidebar-link.active {
|
||||||
background: linear-gradient(90deg, rgba(90,138,127,0.2) 0%, rgba(90,138,127,0.03) 100%);
|
background: linear-gradient(90deg, rgba(124, 58, 237, 0.2) 0%, rgba(124, 58, 237, 0.03) 100%);
|
||||||
color: #a8bfb8 !important;
|
color: #c4b5fd !important;
|
||||||
border-left-color: #a8bfb8;
|
border-left-color: #c4b5fd;
|
||||||
}
|
}
|
||||||
.sidebar-link .sidebar-icon {
|
.sidebar-link .sidebar-icon {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|
@ -53,11 +53,11 @@
|
||||||
transition: all 0.25s ease;
|
transition: all 0.25s ease;
|
||||||
}
|
}
|
||||||
.sidebar-link:hover .sidebar-icon {
|
.sidebar-link:hover .sidebar-icon {
|
||||||
background: rgba(90, 138, 127, 0.25);
|
background: rgba(124, 58, 237, 0.25);
|
||||||
transform: scale(1.05);
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
.sidebar-link.active .sidebar-icon {
|
.sidebar-link.active .sidebar-icon {
|
||||||
background: rgba(90, 138, 127, 0.15);
|
background: rgba(124, 58, 237, 0.15);
|
||||||
}
|
}
|
||||||
.sidebar-section-label {
|
.sidebar-section-label {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
|
|
@ -77,13 +77,13 @@
|
||||||
.sidebar-brand-icon {
|
.sidebar-brand-icon {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
background: linear-gradient(135deg, #5A8A7F 0%, #7BA39A 100%);
|
background: linear-gradient(135deg, #7c3aed 0%, #6366f1 100%);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 20px;
|
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 {
|
.sidebar-footer {
|
||||||
border-top: 1px solid rgba(255,255,255,0.06);
|
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">
|
<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
|
👤 Profil Saya
|
||||||
</a>
|
</a>
|
||||||
<form method="POST" action="{{ route('logout') }}">
|
<form method="POST" action="{{ route('logout') }}" class="confirm-logout">
|
||||||
@csrf
|
@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">
|
<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
|
🚪 Logout
|
||||||
|
|
@ -272,6 +272,50 @@
|
||||||
mobileOverlay.addEventListener('click', () => mobileSidebar.classList.add('hidden'));
|
mobileOverlay.addEventListener('click', () => mobileSidebar.classList.add('hidden'));
|
||||||
closeMobileMenu.addEventListener('click', () => mobileSidebar.classList.add('hidden'));
|
closeMobileMenu.addEventListener('click', () => mobileSidebar.classList.add('hidden'));
|
||||||
</script>
|
</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')
|
@yield('scripts')
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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">
|
<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
|
👤 Lihat Profil
|
||||||
</a>
|
</a>
|
||||||
<form method="POST" action="{{ route('logout') }}">
|
<form method="POST" action="{{ route('logout') }}" class="confirm-logout">
|
||||||
@csrf
|
@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">
|
<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
|
🚪 Logout
|
||||||
|
|
@ -272,5 +272,29 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
@ -204,6 +204,14 @@
|
||||||
<a href="{{ $resetUrl }}" class="cta-button">🔑 RESET PASSWORD</a>
|
<a href="{{ $resetUrl }}" class="cta-button">🔑 RESET PASSWORD</a>
|
||||||
</div>
|
</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 -->
|
<!-- Steps -->
|
||||||
<div class="steps">
|
<div class="steps">
|
||||||
<h3>📋 Langkah-Langkah:</h3>
|
<h3>📋 Langkah-Langkah:</h3>
|
||||||
|
|
|
||||||
|
|
@ -32,5 +32,49 @@
|
||||||
{{ $slot }}
|
{{ $slot }}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@
|
||||||
</x-dropdown-link>
|
</x-dropdown-link>
|
||||||
|
|
||||||
<!-- Authentication -->
|
<!-- Authentication -->
|
||||||
<form method="POST" action="{{ route('logout') }}">
|
<form method="POST" action="{{ route('logout') }}" class="confirm-logout">
|
||||||
@csrf
|
@csrf
|
||||||
|
|
||||||
<x-dropdown-link :href="route('logout')"
|
<x-dropdown-link :href="route('logout')"
|
||||||
|
|
@ -85,7 +85,7 @@
|
||||||
</x-responsive-nav-link>
|
</x-responsive-nav-link>
|
||||||
|
|
||||||
<!-- Authentication -->
|
<!-- Authentication -->
|
||||||
<form method="POST" action="{{ route('logout') }}">
|
<form method="POST" action="{{ route('logout') }}" class="confirm-logout">
|
||||||
@csrf
|
@csrf
|
||||||
|
|
||||||
<x-responsive-nav-link :href="route('logout')"
|
<x-responsive-nav-link :href="route('logout')"
|
||||||
|
|
|
||||||
|
|
@ -6,461 +6,292 @@
|
||||||
<title>Hasil Rekomendasi - Sistem Pemilihan Jurusan</title>
|
<title>Hasil Rekomendasi - Sistem Pemilihan Jurusan</title>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
.gradient-maroon {
|
:root {
|
||||||
background: linear-gradient(135deg, #6B7280 0%, #8B95A5 100%);
|
--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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-cream">
|
<body>
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<header class="gradient-maroon text-white shadow-lg sticky top-0 z-50">
|
<header class="header-main py-4 md:py-6">
|
||||||
<div class="container mx-auto px-4 sm:px-6 py-4 sm:py-6 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3 sm:gap-4">
|
<div class="container mx-auto px-4 md:px-6">
|
||||||
<div>
|
<div class="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
||||||
<h1 class="text-xl sm:text-2xl md:text-3xl font-bold">Hasil Rekomendasi</h1>
|
<div>
|
||||||
<p class="text-xs sm:text-sm text-yellow-300 font-semibold mt-1">Sistem Pemilihan Jurusan</p>
|
<h1 class="text-2xl md:text-3xl font-bold">Hasil Analisis Rekomendasi</h1>
|
||||||
</div>
|
<p class="text-sm md:text-base text-teal-100 mt-1">Jurusan terbaik berdasarkan profil Anda</p>
|
||||||
<div class="flex items-center gap-2 sm:gap-4 w-full sm:w-auto">
|
</div>
|
||||||
<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">
|
<div class="flex gap-3">
|
||||||
Kembali ke Dashboard
|
<a href="{{ route('rekomendasi.input') }}" class="btn-primary px-4 py-2 rounded-lg font-medium text-sm hover:shadow-lg transition">
|
||||||
</a>
|
🔄 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>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
<div class="w-full px-4 sm:px-6 py-6 sm:py-8">
|
<main class="container mx-auto px-4 md:px-6 py-8 md:py-12">
|
||||||
<!-- Selamat Section -->
|
<!-- Data Profil Summary -->
|
||||||
<div class="bg-gradient-to-r from-green-600 to-emerald-600 rounded-xl shadow-lg p-8 text-white mb-8">
|
<div class="card p-6 md:p-8 mb-8">
|
||||||
<div class="flex gap-4 items-start">
|
<h2 class="text-2xl font-bold text-primary mb-6">Data Profil Anda</h2>
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Ringkasan Input -->
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
<div class="bg-white rounded-xl shadow-lg p-8 mb-8 border-t-4 border-purple-600">
|
<!-- Nilai Akademik -->
|
||||||
<h3 class="text-2xl font-bold text-gray-900 mb-6">📊 Profil Analisismu</h3>
|
<div class="profile-box">
|
||||||
<div class="grid grid-cols-2 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
<p style="font-size: 0.75rem; color: var(--text-secondary); text-transform: uppercase; font-weight: 600; margin-bottom: 0.5rem;">Nilai Akademik</p>
|
||||||
<div class="bg-gradient-to-br from-purple-50 to-purple-100 p-5 rounded-lg border border-purple-200">
|
<p style="font-size: 1.5rem; font-weight: 700; color: var(--primary); margin-bottom: 0.25rem;">{{ $katNilai }}</p>
|
||||||
<p class="text-xs font-semibold text-purple-600 mb-2">📚 Nilai Akademik</p>
|
<p style="font-size: 0.875rem; color: var(--text-secondary);">Rata-rata: {{ number_format($average, 1) }}%</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>
|
|
||||||
</div>
|
</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>
|
<!-- Minat -->
|
||||||
<p class="text-lg font-bold text-blue-900">{{ $prefStudi ?? '-' }}</p>
|
<div class="profile-box">
|
||||||
<p class="text-xs text-blue-700 mt-1">Gaya Belajar</p>
|
<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>
|
||||||
<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>
|
<!-- Cita-cita -->
|
||||||
<p class="text-lg font-bold text-amber-900">
|
<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>
|
||||||
|
|
||||||
|
<!-- 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))
|
@if(!($isPrestasiFilled ?? true))
|
||||||
Belum Ada
|
Tidak Ada
|
||||||
@elseif($prestasiScore >= 0.8)
|
@elseif($prestasiScore >= 0.8)
|
||||||
Tinggi
|
Tinggi ⭐
|
||||||
@elseif($prestasiScore >= 0.6)
|
@elseif($prestasiScore >= 0.6)
|
||||||
Sedang
|
Sedang 👍
|
||||||
@elseif($prestasiScore > 0)
|
@elseif($prestasiScore > 0)
|
||||||
Cukup
|
Cukup
|
||||||
@else
|
@else
|
||||||
Belum Ada
|
Belum Ada
|
||||||
@endif
|
@endif
|
||||||
</p>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tabel Peringkat Jurusan -->
|
<!-- Top Recommendation -->
|
||||||
<div class="bg-white rounded-lg shadow-lg p-5 sm:p-6 mb-6 sm:mb-8">
|
@if(count($hasilAkhir) > 0)
|
||||||
<h3 class="text-base sm:text-lg font-bold text-maroon mb-3 sm:mb-4">Peringkat Jurusan</h3>
|
<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="overflow-x-auto">
|
<div class="progress-bar mb-4">
|
||||||
<table class="w-full text-sm">
|
<div class="progress-fill" style="width: {{ number_format(($hasilAkhir[0]['skor'] ?? 0) * 100, 1) }}%"></div>
|
||||||
<thead class="gradient-maroon text-white">
|
</div>
|
||||||
<tr>
|
|
||||||
<th class="px-3 sm:px-6 py-2 sm:py-3 text-left text-xs sm:text-sm font-bold">#</th>
|
<p style="font-size: 0.875rem; color: var(--text-secondary); margin-bottom: 1rem;">
|
||||||
<th class="px-3 sm:px-6 py-2 sm:py-3 text-left text-xs sm:text-sm font-bold">Jurusan</th>
|
Berdasarkan analisis Weighted Naive Bayes, jurusan ini adalah pilihan terbaik untuk profil akademik dan minat Anda.
|
||||||
<th class="px-3 sm:px-6 py-2 sm:py-3 text-right text-xs sm:text-sm font-bold">Skor</th>
|
</p>
|
||||||
</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>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
<!-- Visualisasi Progress Bars -->
|
<!-- All Rankings -->
|
||||||
<div class="mt-4 sm:mt-6 space-y-2 sm:space-y-3">
|
<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)
|
@foreach($hasilAkhir as $index => $res)
|
||||||
<div class="flex flex-col gap-1">
|
<div class="result-item {{ $index == 0 ? 'top-1' : '' }}">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex items-start justify-between mb-3">
|
||||||
<span class="text-xs sm:text-sm font-semibold text-gray-700">{{ $res['jurusan'] ?? '-' }}</span>
|
<div class="flex items-center gap-4">
|
||||||
<span class="text-xs sm:text-sm font-bold text-maroon">{{ number_format(($res['skor'] ?? 0) * 100, 1) }}%</span>
|
<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>
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Detail Rekomendasi Utama -->
|
<!-- Method Explanation -->
|
||||||
@if(count($hasilAkhir) > 0)
|
<div class="card p-6 md:p-8 mt-8 bg-gradient-to-r from-cyan-50 to-blue-50">
|
||||||
<div class="bg-white rounded-lg shadow-lg p-5 sm:p-8 mb-6 sm:mb-8 border-l-4 border-yellow-400">
|
<h3 class="text-lg font-bold text-primary mb-4 flex items-center gap-2">
|
||||||
@php
|
<span>🔬</span> Metode Analisis
|
||||||
$topRecommendation = $hasilAkhir[0];
|
</h3>
|
||||||
$detail = $topRecommendation['detail'] ?? [];
|
<p style="font-size: 0.875rem; color: var(--text-secondary); line-height: 1.6;">
|
||||||
@endphp
|
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%)
|
||||||
<div class="flex flex-col sm:flex-row items-start gap-3 sm:gap-4 mb-4 sm:mb-6">
|
<br>Jika prestasi tidak diisi, bobot distribusi ulang ke 4 kriteria lainnya untuk hasil yang akurat.
|
||||||
<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.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
@ -6,129 +6,227 @@
|
||||||
<title>Input Data - Sistem Pemilihan Jurusan</title>
|
<title>Input Data - Sistem Pemilihan Jurusan</title>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||||
<style>
|
<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;
|
.btn-primary:hover {
|
||||||
box-shadow: 0 0 0 3px rgba(107, 114, 128, 0.1);
|
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 {
|
.input-error {
|
||||||
border-color: #ef4444 !important;
|
border-color: var(--error) !important;
|
||||||
background-color: #fef2f2 !important;
|
background-color: #fff5f5 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-valid {
|
.input-valid {
|
||||||
border-color: #10b981 !important;
|
border-color: var(--success) !important;
|
||||||
background-color: #f0fdf4 !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 {
|
.validation-message {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
margin-top: 0.25rem;
|
margin-top: 0.375rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
animation: slideIn 0.3s ease-out;
|
animation: slideIn 0.3s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes slideIn {
|
@keyframes slideIn {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(-5px);
|
transform: translateY(-4px);
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0);
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-cream">
|
<body>
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<header class="gradient-maroon text-white shadow-lg sticky top-0 z-50">
|
<header class="header-main py-4 md:py-6">
|
||||||
<div class="container mx-auto px-4 sm:px-6 py-4 sm:py-6 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3 sm:gap-4">
|
<div class="container mx-auto px-4 md:px-6">
|
||||||
<div>
|
<div class="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
||||||
<h1 class="text-xl sm:text-2xl md:text-3xl font-bold">Input Data Rekomendasi</h1>
|
<div>
|
||||||
<p class="text-xs sm:text-sm text-yellow-300 font-semibold mt-1">Sistem Pemilihan Jurusan</p>
|
<h1 class="text-2xl md:text-3xl font-bold">Form Analisis Jurusan</h1>
|
||||||
</div>
|
<p class="text-sm md:text-base text-teal-100 mt-1">Temukan jurusan yang paling sesuai dengan minat dan potensi Anda</p>
|
||||||
<div class="flex items-center gap-2 sm:gap-4 w-full sm:w-auto">
|
</div>
|
||||||
<a href="{{ url('/dashboard') }}" class="block sm:inline-block flex-1 sm:flex-none text-center bg-yellow-400 text-maroon font-bold py-2 px-3 sm:px-4 rounded-lg hover:bg-yellow-300 transition text-xs sm:text-sm">
|
<a href="{{ url('/dashboard') }}" class="btn-primary px-4 py-2 rounded-lg font-medium text-sm md:text-base hover:shadow-lg transition">
|
||||||
Kembali ke Dashboard
|
← Kembali
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
<div class="w-full px-4 sm:px-6 py-6 sm:py-8">
|
<main class="container mx-auto px-4 md:px-6 py-8 md:py-12">
|
||||||
<!-- Hero Intro -->
|
<!-- Info Banner -->
|
||||||
<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">
|
<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);">
|
||||||
<h2 class="text-3xl sm:text-4xl font-bold mb-3 sm:mb-4">Kuis Cerdas Menemukan Jurusanmu 🚀</h2>
|
<div style="display: flex; gap: 1rem;">
|
||||||
<p class="text-base sm:text-lg text-blue-100 mb-4 sm:mb-6 leading-relaxed">
|
<div style="font-size: 2rem; flex-shrink: 0;">🎯</div>
|
||||||
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>
|
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-bold text-amber-900 mb-3 text-lg">Tips Agar Hasil Lebih Akurat</h3>
|
<h2 class="text-lg font-semibold mb-2 text-primary">Petunjuk Pengisian</h2>
|
||||||
<ul class="text-sm text-amber-800 space-y-2">
|
<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>
|
||||||
<li>✓ Jawab semua pertanyaan dengan jujur dan sebenar-benarnya</li>
|
<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>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Form Card -->
|
<!-- 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())
|
@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="mb-6 p-4 rounded-lg bg-red-50 border-l-4" style="border-left-color: var(--error);">
|
||||||
<div class="flex items-start gap-3">
|
<div style="display: flex; gap: 1rem;">
|
||||||
<span class="text-2xl flex-shrink-0">❌</span>
|
<div style="font-size: 1.5rem; flex-shrink: 0;">❌</div>
|
||||||
<div class="flex-1">
|
<div>
|
||||||
<h3 class="text-red-700 font-bold text-sm sm:text-base mb-3">Terjadi Kesalahan Validasi</h3>
|
<h3 style="color: var(--error); font-weight: 600; margin-bottom: 0.5rem;">Ada Kesalahan Pengisian</h3>
|
||||||
<p class="text-red-600 text-xs sm:text-sm mb-3">Silakan perbaiki kesalahan berikut sebelum melanjutkan:</p>
|
<ul style="list-style: none; padding: 0; margin: 0;">
|
||||||
<ul class="list-disc list-inside space-y-2 text-red-600 text-xs sm:text-sm bg-white bg-opacity-50 p-3 rounded">
|
|
||||||
@foreach ($errors->all() as $error)
|
@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
|
@endforeach
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -136,182 +234,136 @@
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@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
|
@csrf
|
||||||
|
|
||||||
{{-- ============================================ --}}
|
<!-- NILAI AKADEMIK -->
|
||||||
{{-- KRITERIA 1: NILAI MATA PELAJARAN --}}
|
<section>
|
||||||
{{-- ============================================ --}}
|
<h3 class="section-title">
|
||||||
<div class="p-6 rounded-lg border-2 border-gray-200 bg-gray-50">
|
<span style="font-size: 1.25rem;">1️⃣</span> Nilai Akademik
|
||||||
<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>
|
<span style="color: var(--error);">*</span>
|
||||||
<p class="text-sm text-gray-600 mb-4">
|
</h3>
|
||||||
|
<p class="section-desc">
|
||||||
@if(isset($student) && $student->kelompok_asal == 'IPA')
|
@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
|
@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
|
@endif
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@if(isset($student) && $student->kelompok_asal == 'IPA')
|
@if(isset($student) && $student->kelompok_asal == 'IPA')
|
||||||
{{-- SISWA IPA: Matematika, Fisika, Kimia, Biologi --}}
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
@foreach(['mtk' => 'Matematika', 'fisika' => 'Fisika', 'kimia' => 'Kimia', 'biologi' => 'Biologi'] as $field => $label)
|
||||||
<div class="input-wrapper">
|
<div class="input-field">
|
||||||
<label for="mtk" class="block text-sm font-semibold text-gray-700 mb-2">Matematika <span class="text-red-500">*</span></label>
|
<label>{{ $label }}</label>
|
||||||
<input id="mtk" type="number" name="mtk" min="0" max="100" value="{{ old('mtk') }}" placeholder="Nilai: 85" required
|
<input type="number" name="{{ $field }}" min="0" max="100" value="{{ old($field) }}" placeholder="0-100" 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">
|
class="focus-primary @error($field) input-error @enderror">
|
||||||
@error('mtk')
|
@error($field)
|
||||||
<span class="validation-message text-red-600">⚠️ {{ $message }}</span>
|
<span class="validation-message" style="color: var(--error);">⚠️ {{ $message }}</span>
|
||||||
@enderror
|
@enderror
|
||||||
</div>
|
</div>
|
||||||
<div class="input-wrapper">
|
@endforeach
|
||||||
<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>
|
</div>
|
||||||
@else
|
@else
|
||||||
{{-- SISWA IPS: Ekonomi, Geografi, Sosiologi, Sejarah --}}
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
@foreach(['ekonomi' => 'Ekonomi', 'geografi' => 'Geografi', 'sosiologi' => 'Sosiologi', 'sejarah' => 'Sejarah'] as $field => $label)
|
||||||
<div class="input-wrapper">
|
<div class="input-field">
|
||||||
<label for="ekonomi" class="block text-sm font-semibold text-gray-700 mb-2">Ekonomi <span class="text-red-500">*</span></label>
|
<label>{{ $label }}</label>
|
||||||
<input id="ekonomi" type="number" name="ekonomi" min="0" max="100" value="{{ old('ekonomi') }}" placeholder="Nilai: 82" required
|
<input type="number" name="{{ $field }}" min="0" max="100" value="{{ old($field) }}" placeholder="0-100" 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">
|
class="focus-primary @error($field) input-error @enderror">
|
||||||
@error('ekonomi')
|
@error($field)
|
||||||
<span class="validation-message text-red-600">⚠️ {{ $message }}</span>
|
<span class="validation-message" style="color: var(--error);">⚠️ {{ $message }}</span>
|
||||||
@enderror
|
@enderror
|
||||||
</div>
|
</div>
|
||||||
<div class="input-wrapper">
|
@endforeach
|
||||||
<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>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||||
{{-- ============================================ --}}
|
<!-- MINAT -->
|
||||||
{{-- KRITERIA 2: MINAT SISWA --}}
|
<section>
|
||||||
{{-- ============================================ --}}
|
<h3 class="section-title">
|
||||||
<div class="p-6 rounded-lg border-2 border-gray-200 bg-gray-50">
|
<span style="font-size: 1.25rem;">2️⃣</span> Bidang Minat
|
||||||
<h3 class="font-bold text-lg sm:text-xl text-blue-700 mb-2">2️⃣ Minat Siswa <span class="text-red-500">*</span></h3>
|
<span style="color: var(--error);">*</span>
|
||||||
<p class="text-sm text-gray-600 mb-4">Tuliskan bidang atau kegiatan yang Anda minati / sukai.</p>
|
</h3>
|
||||||
<div class="input-wrapper">
|
<p class="section-desc">Tuliskan bidang atau kegiatan yang Anda minati. Contoh: coding, pertanian, seni, dll.</p>
|
||||||
<label for="minat" class="block text-sm font-semibold text-gray-700 mb-2">Bidang Minat</label>
|
<div class="input-field">
|
||||||
<input id="minat" type="text" name="minat" value="{{ old('minat') }}" placeholder="Contoh: coding, komputer, bisnis, pertanian"
|
<input type="text" name="minat" value="{{ old('minat') }}" placeholder="Minat Anda..." 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('minat') input-error @enderror" required>
|
class="focus-primary @error('minat') input-error @enderror">
|
||||||
<p class="text-xs text-gray-500 mt-2">Pisahkan dengan koma jika lebih dari satu minat</p>
|
<span class="input-hint">Minimal 3 karakter, pisahkan dengan koma jika lebih dari satu</span>
|
||||||
@error('minat')
|
@error('minat')
|
||||||
<span class="validation-message text-red-600">⚠️ {{ $message }}</span>
|
<span class="validation-message" style="color: var(--error);">⚠️ {{ $message }}</span>
|
||||||
@enderror
|
@enderror
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
{{-- ============================================ --}}
|
<!-- PREFERENSI STUDI -->
|
||||||
{{-- KRITERIA 3: PREFERENSI STUDI LANJUTAN --}}
|
<section>
|
||||||
{{-- ============================================ --}}
|
<h3 class="section-title">
|
||||||
<div class="p-6 rounded-lg border-2 border-gray-200 bg-gray-50">
|
<span style="font-size: 1.25rem;">3️⃣</span> Preferensi Studi
|
||||||
<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>
|
<span style="color: var(--error);">*</span>
|
||||||
<p class="text-sm text-gray-600 mb-4">Pilih rumpun jurusan Politeknik Negeri Jember yang paling sesuai.</p>
|
</h3>
|
||||||
<div class="input-wrapper">
|
<p class="section-desc">Pilih rumpun jurusan yang paling Anda minati untuk studi lanjutan.</p>
|
||||||
<label for="pref_studi" class="block text-sm font-semibold text-gray-700 mb-2">Arah Rumpun Jurusan Tujuan</label>
|
<div class="input-field">
|
||||||
<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>
|
<select name="pref_studi" required class="focus-primary @error('pref_studi') input-error @enderror">
|
||||||
<option value="">-- Pilih Arah Rumpun Jurusan --</option>
|
<option value="">-- Pilih Preferensi --</option>
|
||||||
<option value="Sains & Teknologi" {{ old('pref_studi') == 'Sains & Teknologi' ? 'selected' : '' }}>Sains & Teknologi (contoh: TI, Teknik)</option>
|
<option value="Sains & Teknologi" {{ old('pref_studi') == 'Sains & Teknologi' ? 'selected' : '' }}>Sains & Teknologi</option>
|
||||||
<option value="Pertanian & Lingkungan" {{ old('pref_studi') == 'Pertanian & Lingkungan' ? 'selected' : '' }}>Pertanian & Lingkungan (contoh: Produksi/Teknologi Pertanian)</option>
|
<option value="Pertanian & Lingkungan" {{ old('pref_studi') == 'Pertanian & Lingkungan' ? 'selected' : '' }}>Pertanian & Lingkungan</option>
|
||||||
<option value="Kesehatan & Ilmu Hayat" {{ old('pref_studi') == 'Kesehatan & Ilmu Hayat' ? 'selected' : '' }}>Kesehatan & Ilmu Hayat (contoh: rumpun Kesehatan)</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 (contoh: Akuntansi, Manajemen Agribisnis)</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 (contoh: Bahasa, Komunikasi, Pariwisata)</option>
|
<option value="Sosial & Humaniora" {{ old('pref_studi') == 'Sosial & Humaniora' ? 'selected' : '' }}>Sosial & Humaniora</option>
|
||||||
</select>
|
</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')
|
@error('pref_studi')
|
||||||
<span class="validation-message text-red-600">⚠️ {{ $message }}</span>
|
<span class="validation-message" style="color: var(--error);">⚠️ {{ $message }}</span>
|
||||||
@enderror
|
@enderror
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
{{-- ============================================ --}}
|
|
||||||
{{-- 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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Submit Button -->
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||||
<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">
|
<!-- CITA-CITA -->
|
||||||
<p class="text-sm text-gray-700 mb-4 leading-relaxed">
|
<section>
|
||||||
⏳ 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.
|
<h3 class="section-title">
|
||||||
</p>
|
<span style="font-size: 1.25rem;">4️⃣</span> Cita-cita / Karir Impian
|
||||||
<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">
|
<span style="color: var(--error);">*</span>
|
||||||
✨ Lihat Rekomendasi Jurusan
|
</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>
|
</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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -360,47 +412,24 @@ class="block w-full px-4 py-2 border border-gray-300 rounded-lg focus-maroon foc
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@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">
|
<!-- Footer Info -->
|
||||||
<div class="flex gap-4">
|
<div class="card p-4 md:p-5 mt-8 bg-gradient-to-r from-teal-50 to-cyan-50">
|
||||||
<div class="text-3xl flex-shrink-0">🤖</div>
|
<p style="font-size: 0.875rem; color: var(--text-secondary);">
|
||||||
<div>
|
<strong>📊 Metode Analisis:</strong> Weighted Naive Bayes dengan 5 kriteria yang diseimbangkan berdasarkan data historis siswa Polije.
|
||||||
<h3 class="font-bold text-blue-900 mb-2 text-lg">Tentang Sistem Rekomendasi Kami</h3>
|
</p>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</main>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
const form = document.querySelector('form');
|
const form = document.querySelector('form');
|
||||||
const submitBtn = form?.querySelector('button[type="submit"]');
|
const submitBtn = form?.querySelector('button[type="submit"]');
|
||||||
|
|
||||||
// Validasi input fields
|
|
||||||
const inputs = form?.querySelectorAll('input, select, textarea');
|
const inputs = form?.querySelectorAll('input, select, textarea');
|
||||||
|
|
||||||
inputs?.forEach(input => {
|
inputs?.forEach(input => {
|
||||||
// Validasi pada change event
|
input.addEventListener('change', () => validateField(input));
|
||||||
input.addEventListener('change', function() {
|
input.addEventListener('blur', () => validateField(input));
|
||||||
validateField(this);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Validasi pada blur event
|
|
||||||
input.addEventListener('blur', function() {
|
|
||||||
validateField(this);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Remove error on input
|
|
||||||
input.addEventListener('input', function() {
|
input.addEventListener('input', function() {
|
||||||
if (this.classList.contains('input-error')) {
|
if (this.classList.contains('input-error')) {
|
||||||
validateField(this);
|
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) {
|
form?.addEventListener('submit', function(e) {
|
||||||
let isValid = true;
|
let isValid = true;
|
||||||
|
|
||||||
inputs?.forEach(input => {
|
inputs?.forEach(input => {
|
||||||
if (!validateField(input)) {
|
if (!validateField(input)) {
|
||||||
isValid = false;
|
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) {
|
if (!isValid) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
// Scroll ke error pertama
|
|
||||||
const firstError = form.querySelector('.input-error');
|
const firstError = form.querySelector('.input-error');
|
||||||
if (firstError) {
|
if (firstError) {
|
||||||
firstError.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
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) {
|
function validateField(field) {
|
||||||
const value = field.value.trim();
|
const value = field.value.trim();
|
||||||
const isRequired = field.hasAttribute('required');
|
const isRequired = field.hasAttribute('required');
|
||||||
const name = field.name;
|
|
||||||
const type = field.type;
|
const type = field.type;
|
||||||
let errorMsg = '';
|
|
||||||
let isValid = true;
|
let isValid = true;
|
||||||
|
|
||||||
// Remove previous error/valid styling
|
|
||||||
field.classList.remove('input-error', 'input-valid');
|
field.classList.remove('input-error', 'input-valid');
|
||||||
const existingMsg = field.parentElement.querySelector('.validation-message');
|
const existingMsg = field.parentElement.querySelector('.validation-message');
|
||||||
if (existingMsg) {
|
if (existingMsg) existingMsg.remove();
|
||||||
existingMsg.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validasi untuk field yang required
|
|
||||||
if (isRequired && !value) {
|
if (isRequired && !value) {
|
||||||
errorMsg = '⚠️ ' + field.placeholder?.split('Contoh')[0]?.trim() + ' tidak boleh kosong';
|
|
||||||
isValid = false;
|
isValid = false;
|
||||||
}
|
} else if (type === 'number' && value) {
|
||||||
|
const num = parseFloat(value);
|
||||||
// Validasi untuk number fields
|
if (isNaN(num) || num < 0 || num > 100) {
|
||||||
if (type === 'number' && value) {
|
|
||||||
const numValue = parseFloat(value);
|
|
||||||
const min = parseFloat(field.min || 0);
|
|
||||||
const max = parseFloat(field.max || 100);
|
|
||||||
|
|
||||||
if (isNaN(numValue)) {
|
|
||||||
errorMsg = '⚠️ Masukkan angka yang valid (0-100)';
|
|
||||||
isValid = false;
|
|
||||||
} else if (numValue < min || numValue > max) {
|
|
||||||
errorMsg = '⚠️ Nilai harus antara ' + min + ' - ' + max;
|
|
||||||
isValid = false;
|
isValid = false;
|
||||||
}
|
}
|
||||||
}
|
} else if ((field.name === 'minat' || field.name === 'cita_cita') && value && value.length < 3) {
|
||||||
|
|
||||||
// Validasi untuk text fields (min length)
|
|
||||||
if ((name === 'minat' || name === 'cita_cita') && value && value.length < 3) {
|
|
||||||
errorMsg = '⚠️ Minimal 3 karakter, jelaskan lebih detail';
|
|
||||||
isValid = false;
|
isValid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validasi untuk select (pref_studi)
|
if (!isValid && (value || isRequired)) {
|
||||||
if (name === 'pref_studi' && !value) {
|
|
||||||
errorMsg = '⚠️ Pilih salah satu preferensi studi';
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply styling
|
|
||||||
if (!isValid && value) {
|
|
||||||
field.classList.add('input-error');
|
field.classList.add('input-error');
|
||||||
} else if (isValid && (isRequired || value)) {
|
} else if (isValid && (isRequired || value)) {
|
||||||
field.classList.add('input-valid');
|
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;
|
return isValid || !isRequired;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loading state pada submit
|
|
||||||
form?.addEventListener('submit', function() {
|
form?.addEventListener('submit', function() {
|
||||||
submitBtn.disabled = true;
|
if (submitBtn) {
|
||||||
submitBtn.innerHTML = '<span class="inline-flex items-center">⏳ Menganalisis data...</span>';
|
submitBtn.disabled = true;
|
||||||
|
submitBtn.innerHTML = '⏳ Menganalisis...';
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -82,6 +82,9 @@
|
||||||
Route::get('/profil', [AdminController::class, 'profil'])->name('profil');
|
Route::get('/profil', [AdminController::class, 'profil'])->name('profil');
|
||||||
Route::put('/profil', [AdminController::class, 'updateProfil'])->name('profil.update');
|
Route::put('/profil', [AdminController::class, 'updateProfil'])->name('profil.update');
|
||||||
Route::put('/profil/password', [AdminController::class, 'updatePassword'])->name('profil.password');
|
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)
|
// BK Routes (role-based access control)
|
||||||
|
|
@ -114,3 +117,8 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
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