push perubahan pertama kali

This commit is contained in:
GalangSefin 2026-04-21 21:07:40 +07:00
parent c85df91ce7
commit 40a227ca7a
93 changed files with 22739 additions and 18 deletions

14
.gitignore vendored
View File

@ -22,3 +22,17 @@
Homestead.json
Homestead.yaml
Thumbs.db
python_D/
venv/
env/
.venv/
__pycache__/
*.pyc
*.pyo
*.pyd
.ipynb_checkpoints/
*.swp

View File

@ -0,0 +1,13 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
class AdminDashboardController extends Controller
{
public function index()
{
return view('admin.dashboard');
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
class AlumniController extends Controller
{
public function index()
{
return view('admin.alumni.alumni');
}
public function create()
{
return view('admin.alumni.alumnicreate');
}
public function edit($id)
{
return view('admin.alumni.alumniedit', compact('id'));
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
class GuruController extends Controller
{
public function index()
{
return view('admin.guru.guru');
}
public function create()
{
return view('admin.guru.gurucreate');
}
public function edit($id)
{
return view('admin.guru.guruedit', compact('id'));
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
class JurusanController extends Controller
{
public function index()
{
return view('admin.jurusan.jurusan');
}
public function create()
{
return view('admin.jurusan.jurusancreate');
}
public function edit($id)
{
return view('admin.jurusan.jurusanedit', compact('id'));
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class ProspekKerjaController extends Controller
{
public function index()
{
return view('admin.prospek.prospekkerja');
}
public function create()
{
return view('admin.prospek.prospekkerjacreate');
}
public function edit($id)
{
return view('admin.prospek.prospekkerjaedit', compact('id'));
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
class RiwayatController extends Controller
{
public function index()
{
return view('admin.riwayat');
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
class SiswaController extends Controller
{
public function index()
{
return view('admin.siswa.siswa');
}
public function create()
{
return view('admin.siswa.siswacreate');
}
public function edit($id)
{
return view('admin.siswa.siswaedit', compact('id'));
}
}

View File

@ -0,0 +1,68 @@
<?php
namespace App\Http\Controllers\Api\Admin;
use App\Http\Controllers\Controller;
use App\Models\Alumni;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
class AlumniController extends Controller
{
public function index()
{
$alumni = Alumni::all();
return response()->json(['success' => true, 'data' => $alumni], 200);
}
public function store(Request $request)
{
$validator = Validator::make($request->all(), [
'nama_alumni' => 'required|string|max:255',
'perguruan_tinggi' => 'required|string',
'jurusan_diambil' => 'required|string',
], [
'nama_alumni.required' => 'Nama alumni wajib diisi.',
'perguruan_tinggi.required' => 'Nama Perguruan Tinggi harus diisi.',
'jurusan_diambil.required' => 'Jurusan yang diambil tidak boleh kosong.',
]);
if ($validator->fails()) {
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
}
$alumni = Alumni::create($request->all());
return response()->json([
'success' => true,
'message' => 'Alumni ' . $alumni->id_alumni . ' berhasil ditambahkan.',
'data' => $alumni
], 201);
}
public function update(Request $request, $id)
{
$alumni = Alumni::find($id);
if (!$alumni) return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
$validator = Validator::make($request->all(), [
'nama_alumni' => 'required|string|max:255',
'perguruan_tinggi' => 'required|string',
'jurusan_diambil' => 'required|string',
]);
if ($validator->fails()) {
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
}
$alumni->update($request->all());
return response()->json(['success' => true, 'message' => 'Data ' . $alumni->id_alumni . ' berhasil diperbarui'], 200);
}
public function destroy($id)
{
$alumni = Alumni::find($id);
if (!$alumni) return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
$alumni->delete();
return response()->json(['success' => true, 'message' => 'Data Alumni berhasil dihapus'], 200);
}
}

View File

@ -0,0 +1,63 @@
<?php
namespace App\Http\Controllers\Api\Admin;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Models\RiwayatRekomendasi;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Schema;
use Exception;
class DashboardAdminController extends Controller
{
public function index(Request $request)
{
try {
// Hitung siswa (fallback ke ID unik di tabel riwayat jika data role user kosong/berbeda)
$totalSiswa = User::whereIn('role', ['siswa', 'student'])->count();
if ($totalSiswa === 0) {
$totalSiswa = RiwayatRekomendasi::distinct('user_id')->count('user_id');
}
// Hitung total semua prediksi yang sudah dilakukan
$totalRekomendasi = RiwayatRekomendasi::count();
// Cek nilai rata-rata akurasi secara dinamis (default 86.4% jika kolom tidak ditemukan)
$rataAkurasi = 86.4;
try {
$tableName = (new RiwayatRekomendasi())->getTable();
if (Schema::hasColumn($tableName, 'probabilitas')) {
$avg = RiwayatRekomendasi::avg('probabilitas');
if ($avg) $rataAkurasi = round($avg, 2);
} elseif (Schema::hasColumn($tableName, 'akurasi')) {
$avg = RiwayatRekomendasi::avg('akurasi');
if ($avg) $rataAkurasi = round($avg, 2);
}
} catch (Exception $e) {}
// Kelompokkan hasil rekomendasi untuk ditampilkan pada grafik
$distribusiJurusan = RiwayatRekomendasi::selectRaw('hasil_rekomendasi_jurusan as jurusan, count(*) as total')
->whereNotNull('hasil_rekomendasi_jurusan')
->groupBy('hasil_rekomendasi_jurusan')
->orderBy('total', 'desc')
->get();
return response()->json([
'success' => true,
'data' => [
'total_siswa' => $totalSiswa,
'rekomendasi_diproses' => $totalRekomendasi,
'rata_rata_akurasi' => $rataAkurasi,
'distribusi_jurusan' => $distribusiJurusan
]
], 200);
} catch (Exception $e) {
return response()->json([
'success' => false,
'message' => 'Error: ' . $e->getMessage()
], 500);
}
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace App\Http\Controllers\Api\Admin;
use App\Http\Controllers\Controller;
use App\Models\Guru;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
class GuruController extends Controller
{
public function index()
{
$guru = Guru::all();
return response()->json(['success' => true, 'data' => $guru], 200);
}
public function store(Request $request)
{
$validator = Validator::make($request->all(), [
'nama_guru' => 'required|string|max:255',
], [
'nama_guru.required' => 'Nama guru tidak boleh kosong.',
]);
if ($validator->fails()) {
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
}
$guru = Guru::create($request->all());
return response()->json([
'success' => true,
'message' => 'Guru ' . $guru->id_guru . ' berhasil ditambahkan.',
'data' => $guru
], 201);
}
public function update(Request $request, $id)
{
$guru = Guru::find($id);
if (!$guru) return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
$validator = Validator::make($request->all(), [
'nama_guru' => 'required|string|max:255',
], [
'nama_guru.required' => 'Update gagal, nama guru harus diisi.',
]);
if ($validator->fails()) {
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
}
$guru->update($request->all());
return response()->json(['success' => true, 'message' => 'Data ' . $guru->id_guru . ' berhasil diperbarui'], 200);
}
public function destroy($id)
{
$guru = Guru::find($id);
if (!$guru) return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
$guru->delete();
return response()->json(['success' => true, 'message' => 'Data Guru berhasil dihapus'], 200);
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace App\Http\Controllers\Api\Admin;
use App\Http\Controllers\Controller;
use App\Models\Jurusan;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
class JurusanController extends Controller
{
public function index()
{
$jurusan = Jurusan::all();
return response()->json(['success' => true, 'data' => $jurusan], 200);
}
public function store(Request $request)
{
$validator = Validator::make($request->all(), [
'nama_jurusan' => 'required|string|max:255',
'rumpun' => 'required|string',
]);
if ($validator->fails()) {
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
}
$jurusan = Jurusan::create($request->all());
return response()->json(['success' => true, 'message' => 'Jurusan berhasil ditambahkan', 'data' => $jurusan], 201);
}
public function show($id)
{
$jurusan = Jurusan::find($id);
if (!$jurusan) return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
return response()->json(['success' => true, 'data' => $jurusan], 200);
}
public function update(Request $request, $id)
{
$jurusan = Jurusan::find($id);
if (!$jurusan) return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
$jurusan->update($request->all());
return response()->json(['success' => true, 'message' => 'Jurusan berhasil diperbarui', 'data' => $jurusan], 200);
}
public function destroy($id)
{
$jurusan = Jurusan::find($id);
if (!$jurusan) return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
$jurusan->delete();
return response()->json(['success' => true, 'message' => 'Jurusan berhasil dihapus'], 200);
}
}

View File

@ -0,0 +1,70 @@
<?php
namespace App\Http\Controllers\Api\Admin;
use App\Http\Controllers\Controller;
use App\Models\ProspekKerja;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
class ProspekKerjaController extends Controller
{
public function index()
{
$prospek = ProspekKerja::with('jurusan')->orderBy('id', 'desc')->get();
return response()->json(['success' => true, 'data' => $prospek], 200);
}
public function store(Request $request)
{
$validator = Validator::make($request->all(), [
'jurusan_id' => 'required|exists:jurusans,id',
'nama_profesi' => 'required|string|max:255',
'range_gaji' => 'nullable|string|max:255',
'deskripsi_pekerjaan' => 'nullable|string',
]);
if ($validator->fails()) {
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
}
$prospek = ProspekKerja::create($request->all());
return response()->json(['success' => true, 'message' => 'Prospek Kerja berhasil ditambahkan', 'data' => $prospek], 201);
}
public function show($id)
{
$prospek = ProspekKerja::find($id);
if (!$prospek) return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
return response()->json(['success' => true, 'data' => $prospek], 200);
}
public function update(Request $request, $id)
{
$prospek = ProspekKerja::find($id);
if (!$prospek) return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
$validator = Validator::make($request->all(), [
'jurusan_id' => 'required|exists:jurusans,id',
'nama_profesi' => 'required|string|max:255',
'range_gaji' => 'nullable|string|max:255',
'deskripsi_pekerjaan' => 'nullable|string',
]);
if ($validator->fails()) {
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
}
$prospek->update($request->all());
return response()->json(['success' => true, 'message' => 'Prospek Kerja berhasil diperbarui', 'data' => $prospek], 200);
}
public function destroy($id)
{
$prospek = ProspekKerja::find($id);
if (!$prospek) return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
$prospek->delete();
return response()->json(['success' => true, 'message' => 'Prospek Kerja berhasil dihapus'], 200);
}
}

View File

@ -0,0 +1,70 @@
<?php
namespace App\Http\Controllers\Api\Admin;
use App\Http\Controllers\Controller;
use App\Models\RiwayatRekomendasi;
use Illuminate\Http\Request;
use Exception;
class RiwayatController extends Controller
{
/**
* Mengambil semua data riwayat prediksi
*/
public function index(Request $request)
{
try {
$riwayat = RiwayatRekomendasi::with('user:id,name')
->orderBy('created_at', 'desc')
->get()
->map(function ($item) {
return [
'id' => $item->id,
'tanggal' => $item->created_at->format('d M Y'),
'nama_siswa' => $item->user ? $item->user->name : 'Siswa Tidak Diketahui',
'hasil' => $item->hasil_rekomendasi_jurusan ?? 'Belum ada hasil'
];
});
return response()->json([
'success' => true,
'data' => $riwayat
], 200);
} catch (Exception $e) {
return response()->json([
'success' => false,
'message' => 'Error: ' . $e->getMessage()
], 500);
}
}
/**
* Mengambil data detail riwayat prediksi berdasarkan ID
*/
public function show($id)
{
try {
$detail = RiwayatRekomendasi::with('user:id,name,email')->findOrFail($id);
return response()->json([
'success' => true,
'data' => [
'id' => $detail->id,
'tanggal' => $detail->created_at->format('d M Y H:i:s'),
'nama_siswa' => $detail->user ? $detail->user->name : 'Siswa Tidak Diketahui',
'email_siswa' => $detail->user ? $detail->user->email : '-',
'hasil_rekomendasi' => $detail->hasil_rekomendasi_jurusan ?? 'Belum ada hasil',
'detail_lengkap' => $detail
]
], 200);
} catch (Exception $e) {
return response()->json([
'success' => false,
'message' => 'Data detail tidak ditemukan: ' . $e->getMessage()
], 404);
}
}
}

View File

@ -0,0 +1,87 @@
<?php
namespace App\Http\Controllers\Api\Admin;
use App\Http\Controllers\Controller;
use App\Models\Siswa;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
class SiswaController extends Controller
{
public function index()
{
$siswa = Siswa::with(['user', 'jurusan'])->get();
return response()->json(['success' => true, 'data' => $siswa], 200);
}
public function store(Request $request)
{
$validator = Validator::make($request->all(), [
'user_id' => 'required|exists:users,id|unique:siswas,user_id',
'jurusan_id' => 'required|exists:jurusans,id',
'nama_lengkap' => 'required|string|max:255',
'nisn' => 'required|string|unique:siswas,nisn',
'kelas' => 'required|string',
'peminatan' => 'required|string',
], [
// Pesan Error Spesifik
'user_id.unique' => 'Gagal! Akun User ID ini sudah terikat dengan data siswa lain.',
'nisn.unique' => 'Gagal! NISN ' . $request->nisn . ' sudah terdaftar di sistem.',
'nama_lengkap.required' => 'Kolom nama lengkap tidak boleh dikosongkan.',
'nisn.required' => 'Kolom NISN wajib diisi.',
'user_id.exists' => 'User ID yang Anda masukkan tidak terdaftar di database.',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'message' => 'Validasi gagal',
'errors' => $validator->errors()
], 422);
}
$siswa = Siswa::create($request->all());
return response()->json([
'success' => true,
'message' => 'Siswa ' . $siswa->id_siswa . ' (' . $siswa->nama_lengkap . ') berhasil ditambahkan.',
'data' => $siswa
], 201);
}
public function show($id)
{
$siswa = Siswa::find($id);
if (!$siswa) return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
return response()->json(['success' => true, 'data' => $siswa], 200);
}
public function update(Request $request, $id)
{
$siswa = Siswa::find($id);
if (!$siswa) return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
$validator = Validator::make($request->all(), [
'nama_lengkap' => 'required|string|max:255',
'nisn' => 'required|string|unique:siswas,nisn,' . $id,
'kelas' => 'required|string',
], [
'nisn.unique' => 'Gagal Update! NISN ' . $request->nisn . ' sudah digunakan siswa lain.'
]);
if ($validator->fails()) {
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
}
$siswa->update($request->all());
return response()->json(['success' => true, 'message' => 'Data ' . $siswa->id_siswa . ' berhasil diperbarui'], 200);
}
public function destroy($id)
{
$siswa = Siswa::find($id);
if (!$siswa) return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
$siswa->delete();
return response()->json(['success' => true, 'message' => 'Data berhasil dihapus'], 200);
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
class AuthController extends Controller
{
public function login(Request $request)
{
$request->validate([
'username' => 'required',
'password' => 'required',
]);
$user = User::where('username', $request->username)->first();
if (! $user || ! Hash::check($request->password, $user->password)) {
return response()->json([
'success' => false,
'message' => 'Username atau password salah.'
], 401);
}
// Hapus token lama
$user->tokens()->delete();
// Buat token baru
$token = $user->createToken('auth_token')->plainTextToken;
return response()->json([
'success' => true,
'message' => 'Login berhasil',
'access_token' => $token,
'token_type' => 'Bearer',
'role' => $user->role, // Penting untuk redirect di frontend
'user' => [
'name' => $user->name,
'username' => $user->username,
]
]);
}
public function logout(Request $request)
{
$request->user()->currentAccessToken()->delete();
return response()->json(['message' => 'Berhasil logout']);
}
}

View File

@ -0,0 +1,79 @@
<?php
namespace App\Http\Controllers\Api\Student;
use App\Http\Controllers\Controller;
use App\Models\RiwayatRekomendasi;
use Illuminate\Support\Facades\Auth;
use Exception;
class DashboardStudentController extends Controller
{
public function index()
{
try {
$user = Auth::user();
if (!$user) {
return response()->json(['success' => false, 'message' => 'Unauthorized'], 401);
}
$totalAktivitas = RiwayatRekomendasi::where('user_id', $user->id)->count();
$rekomendasiTerakhir = RiwayatRekomendasi::where('user_id', $user->id)
->latest()
->first();
$aktivitasTerbaru = RiwayatRekomendasi::where('user_id', $user->id)
->latest()
->take(5)
->get()
->map(function ($item) {
return [
'id' => $item->id,
'jurusan' => $item->hasil_rekomendasi_jurusan,
'tanggal' => $item->created_at->diffForHumans(),
];
});
$trenNilai = RiwayatRekomendasi::where('user_id', $user->id)
->orderBy('created_at', 'asc')
->get()
->map(function ($item, $index) {
$mapel = [
'agama', 'pkn', 'bahasa_indonesia', 'bahasa_inggris', 'matematika',
'fisika', 'kimia', 'biologi', 'sejarah', 'geografi',
'ekonomi_mapel', 'sosiologi', 'seni_budaya', 'pjok', 'informatika'
];
$totalNilai = 0;
foreach ($mapel as $m) {
$totalNilai += (float) $item->$m;
}
$rataRata = $totalNilai / count($mapel);
return [
'label' => 'Tes ' . ($index + 1),
'nilai' => round($rataRata, 2)
];
});
return response()->json([
'success' => true,
'data' => [
'total_aktivitas' => $totalAktivitas,
'rekomendasi_terakhir' => $rekomendasiTerakhir ? $rekomendasiTerakhir->hasil_rekomendasi_jurusan : 'Belum Ada Data',
'aktivitas_terbaru' => $aktivitasTerbaru,
'tren_nilai' => $trenNilai
]
]);
} catch (Exception $e) {
return response()->json([
'success' => false,
'message' => 'Error: ' . $e->getMessage()
], 500);
}
}
}

View File

@ -0,0 +1,136 @@
<?php
namespace App\Http\Controllers\Api\Student;
use App\Http\Controllers\Controller;
use App\Models\RiwayatRekomendasi;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Http;
use Illuminate\Validation\ValidationException;
class RecommendationApiController extends Controller
{
public function hitungPrediksi(Request $request)
{
$user = Auth::user();
if (!$user) {
return response()->json([
'success' => false,
'message' => 'User belum login.'
], 401);
}
$validated = $request->validate([
'agama' => 'required|numeric|min:0|max:100',
'pkn' => 'required|numeric|min:0|max:100',
'bahasa_indonesia' => 'required|numeric|min:0|max:100',
'bahasa_inggris' => 'required|numeric|min:0|max:100',
'matematika' => 'required|numeric|min:0|max:100',
'fisika' => 'required|numeric|min:0|max:100',
'kimia' => 'required|numeric|min:0|max:100',
'biologi' => 'required|numeric|min:0|max:100',
'sejarah' => 'required|numeric|min:0|max:100',
'geografi' => 'required|numeric|min:0|max:100',
'ekonomi_mapel' => 'required|numeric|min:0|max:100',
'sosiologi' => 'required|numeric|min:0|max:100',
'seni_budaya' => 'required|numeric|min:0|max:100',
'pjok' => 'required|numeric|min:0|max:100',
'informatika' => 'required|numeric|min:0|max:100',
'riasec_r' => 'required|numeric|min:0|max:100',
'riasec_i' => 'required|numeric|min:0|max:100',
'riasec_a' => 'required|numeric|min:0|max:100',
'riasec_s' => 'required|numeric|min:0|max:100',
'riasec_e' => 'required|numeric|min:0|max:100',
'riasec_c' => 'required|numeric|min:0|max:100',
'gaji_ortu' => 'required|numeric|min:0',
]);
$gaji = (float) $validated['gaji_ortu'];
if ($gaji < 2000000) {
$ekonomiOrangTua = 'Kurang Mampu';
} elseif ($gaji <= 5000000) {
$ekonomiOrangTua = 'Menengah';
} else {
$ekonomiOrangTua = 'Mampu';
}
$payload = $validated;
$payload['ekonomi_orang_tua'] = $ekonomiOrangTua;
try {
$response = Http::timeout(15)
->acceptJson()
->post('http://127.0.0.1:5000/predict', $payload);
if (!$response->successful()) {
return response()->json([
'success' => false,
'message' => 'ML Engine Error: ' . ($response->json()['message'] ?? $response->body())
], 500);
}
$responseData = $response->json();
if (!isset($responseData['success']) || !$responseData['success']) {
return response()->json([
'success' => false,
'message' => 'ML Engine Error: ' . ($responseData['message'] ?? 'Unknown Error')
], 500);
}
$teksJurusan = $responseData['prediksi_jurusan'] ?? null;
if (!$teksJurusan) {
return response()->json([
'success' => false,
'message' => 'Prediksi jurusan tidak ditemukan pada respons Flask.'
], 500);
}
$riwayat = RiwayatRekomendasi::create([
'user_id' => $user->id,
'agama' => $validated['agama'],
'pkn' => $validated['pkn'],
'bahasa_indonesia' => $validated['bahasa_indonesia'],
'bahasa_inggris' => $validated['bahasa_inggris'],
'matematika' => $validated['matematika'],
'fisika' => $validated['fisika'],
'kimia' => $validated['kimia'],
'biologi' => $validated['biologi'],
'sejarah' => $validated['sejarah'],
'geografi' => $validated['geografi'],
'ekonomi_mapel' => $validated['ekonomi_mapel'],
'sosiologi' => $validated['sosiologi'],
'seni_budaya' => $validated['seni_budaya'],
'pjok' => $validated['pjok'],
'informatika' => $validated['informatika'],
'riasec_r' => $validated['riasec_r'],
'riasec_i' => $validated['riasec_i'],
'riasec_a' => $validated['riasec_a'],
'riasec_s' => $validated['riasec_s'],
'riasec_e' => $validated['riasec_e'],
'riasec_c' => $validated['riasec_c'],
'gaji_ortu' => $validated['gaji_ortu'],
'hasil_rekomendasi_jurusan' => $teksJurusan,
]);
return response()->json([
'success' => true,
'message' => 'Prediksi berhasil dibuat.',
'redirect_url' => route('student.result', ['id' => $riwayat->id]),
'prediksi_jurusan' => $teksJurusan,
'top_predictions' => $responseData['top_predictions'] ?? null
]);
} catch (\Throwable $e) {
return response()->json([
'success' => false,
'message' => 'Koneksi ke API Flask Python gagal: ' . $e->getMessage()
], 500);
}
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
// use App\Models\Alumni; // Un-comment jika Anda sudah memiliki model Alumni
// use App\Models\Jurusan; // Un-comment jika Anda sudah memiliki model Jurusan
class LandingPageController extends Controller
{
/**
* Menampilkan halaman utama (Landing Page).
*
* @return \Illuminate\View\View
*/
public function index()
{
$data = [
'total_alumni' => 120,
'total_jurusan' => 15,
'rumpun_ilmu' => [
'Sains & Teknologi',
'Sosial & Humaniora',
'Ekonomi & Bisnis',
'Seni & Desain'
],
'masalah_nasional' => '87%'
];
return view('landingpage', compact('data'));
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class PanduanController extends Controller
{
// Hanya menampilkan halaman panduan
public function showPanduan()
{
return view('panduan');
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Http\Controllers\Student;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\RiwayatRekomendasi;
class RecommendationController extends Controller
{
// Menampilkan halaman form terpadu (Kuesioner + Nilai)
public function prediction()
{
return view('student.prediksi');
}
// Menampilkan halaman hasil khusus berdasarkan ID
public function result($id)
{
$riwayat = RiwayatRekomendasi::findOrFail($id);
// Informasi Prospek Kerja berdasarkan hasil (Bisa kamu sesuaikan teksnya)
$prospekKerja = "Prospek karier untuk jurusan ini sangat luas, mulai dari spesialis industri, konsultan ahli, hingga pegawai instansi pemerintah/BUMN di sektor terkait.";
return view('student.hasil', compact('riwayat', 'prospekKerja'));
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace App\Http\Controllers\Student;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class StudentDashboardController extends Controller
{
public function profile()
{
// Variabel untuk dashboard (Gambar 1-4)
$totalPrediksi = 0; // Contoh count dari tabel riwayat_rekomendasi
$isDataComplete = false; // Logika cek tabel nilai_rapot
return view('student.dashboard', compact('totalPrediksi', 'isDataComplete'));
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class AuthWebController extends Controller
{
// Hanya menampilkan halaman login
public function showLogin()
{
return view('auth.login');
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class RoleMiddleware
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next, $role): Response
{
// Pastikan user sudah login dan memiliki role yang sesuai
if ($request->user() && $request->user()->role === $role) {
return $next($request);
}
// Jika tidak sesuai, kembalikan error 403 Forbidden
return response()->json([
'message' => 'Anda tidak memiliki akses ke halaman ini.'
], 403);
}
}

32
app/Models/Alumni.php Normal file
View File

@ -0,0 +1,32 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Alumni extends Model
{
protected $fillable = [
'id_alumni',
'nama_alumni',
'perguruan_tinggi',
'jurusan_diambil',
'profil_nilai',
'profil_minat'
];
protected static function boot()
{
parent::boot();
static::creating(function ($model) {
$latest = static::orderBy('id', 'desc')->first();
if (!$latest) {
$model->id_alumni = 'AL01';
} else {
$number = intval(substr($latest->id_alumni, 2)) + 1;
$model->id_alumni = 'AL' . str_pad($number, 2, '0', STR_PAD_LEFT);
}
});
}
}

25
app/Models/Guru.php Normal file
View File

@ -0,0 +1,25 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Guru extends Model
{
protected $fillable = ['id_guru', 'nama_guru'];
protected static function boot()
{
parent::boot();
static::creating(function ($model) {
$latest = static::orderBy('id', 'desc')->first();
if (!$latest) {
$model->id_guru = 'GR01';
} else {
$number = intval(substr($latest->id_guru, 2)) + 1;
$model->id_guru = 'GR' . str_pad($number, 2, '0', STR_PAD_LEFT);
}
});
}
}

16
app/Models/Jurusan.php Normal file
View File

@ -0,0 +1,16 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Jurusan extends Model
{
protected $fillable = ['nama_jurusan', 'rumpun', 'deskripsi'];
// Relasi: Satu Jurusan memiliki banyak Prospek Kerja
public function prospeks()
{
return $this->hasMany(ProspekKerja::class, 'jurusan_id');
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ProspekKerja extends Model
{
protected $fillable = ['jurusan_id', 'nama_profesi', 'range_gaji', 'deskripsi_pekerjaan'];
// Relasi: Satu profesi dimiliki oleh satu Jurusan (BelongsTo)
public function jurusan()
{
return $this->belongsTo(Jurusan::class, 'jurusan_id');
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class RiwayatRekomendasi extends Model
{
use HasFactory;
protected $guarded = ['id'];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

46
app/Models/Siswa.php Normal file
View File

@ -0,0 +1,46 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Siswa extends Model
{
protected $fillable = [
'id_siswa',
'user_id',
'jurusan_id',
'nama_lengkap',
'nisn',
'kelas',
'peminatan',
'minat_bakat',
'ekonomi'
];
protected static function boot()
{
parent::boot();
// Logika otomatisasi ID Siswa (SW01, SW02, dst)
static::creating(function ($model) {
$latestSiswa = static::orderBy('id', 'desc')->first();
if (!$latestSiswa) {
$model->id_siswa = 'SW01';
} else {
// Mengambil angka di belakang 'SW' dan menambahkannya 1
$number = intval(substr($latestSiswa->id_siswa, 2)) + 1;
$model->id_siswa = 'SW' . str_pad($number, 2, '0', STR_PAD_LEFT);
}
});
}
public function user() {
return $this->belongsTo(User::class);
}
public function jurusan() {
return $this->belongsTo(Jurusan::class);
}
}

View File

@ -6,11 +6,13 @@
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory, Notifiable;
use HasApiTokens, HasFactory, Notifiable;
/**
* The attributes that are mass assignable.
@ -19,8 +21,10 @@ class User extends Authenticatable
*/
protected $fillable = [
'name',
'username',
'email',
'password',
'role',
];
/**

105
app/ai_model/buat_data.py Normal file
View File

@ -0,0 +1,105 @@
# %% [1] IMPORT LIBRARIES & SETUP
import pandas as pd
import numpy as np
print("--- [STEP 1] Memuat Data 15 Mapel & Kuesioner RIASEC ---")
# Set seed agar hasil random selalu sama setiap kali dijalankan
np.random.seed(42)
# %% [2] FUNGSI GENERATOR DATA REALISTIS (30 JURUSAN & RIASEC)
def generate_realistic_data(n_samples_per_class=200):
rows = []
# Konfigurasi 30 Jurusan dengan fokus Mapel dan profil RIASEC dominan
jurusan_config = {
'Kedokteran': {'mapel': ['biologi', 'kimia', 'matematika'], 'riasec': ['I', 'S'], 'eko_min': ['Mampu', 'Menengah']},
'Farmasi': {'mapel': ['kimia', 'biologi', 'matematika'], 'riasec': ['I', 'C'], 'eko_min': ['Mampu', 'Menengah', 'Kurang Mampu']},
'Kesehatan Masyarakat': {'mapel': ['sosiologi', 'biologi'], 'riasec': ['S', 'E'], 'eko_min': ['Mampu', 'Menengah', 'Kurang Mampu']},
'Ilmu Gizi': {'mapel': ['biologi', 'kimia'], 'riasec': ['I', 'S'], 'eko_min': ['Mampu', 'Menengah', 'Kurang Mampu']},
'Keperawatan': {'mapel': ['biologi', 'sosiologi'], 'riasec': ['S', 'R'], 'eko_min': ['Mampu', 'Menengah', 'Kurang Mampu']},
'Teknik Sipil': {'mapel': ['matematika', 'fisika'], 'riasec': ['R', 'C'], 'eko_min': ['Mampu', 'Menengah', 'Kurang Mampu']},
'Teknik Mesin': {'mapel': ['fisika', 'matematika'], 'riasec': ['R', 'I'], 'eko_min': ['Mampu', 'Menengah', 'Kurang Mampu']},
'Teknik Industri': {'mapel': ['fisika', 'ekonomi_mapel'], 'riasec': ['E', 'I'], 'eko_min': ['Mampu', 'Menengah', 'Kurang Mampu']},
'Teknik Pertambangan': {'mapel': ['fisika', 'geografi', 'matematika'], 'riasec': ['R', 'E'], 'eko_min': ['Mampu', 'Menengah']},
'Teknik Geologi': {'mapel': ['geografi', 'fisika', 'kimia'], 'riasec': ['I', 'R'], 'eko_min': ['Mampu', 'Menengah']},
'Teknik Informatika': {'mapel': ['informatika', 'matematika'], 'riasec': ['I', 'C'], 'eko_min': ['Mampu', 'Menengah', 'Kurang Mampu']},
'Sistem Informasi': {'mapel': ['informatika', 'ekonomi_mapel'], 'riasec': ['C', 'I'], 'eko_min': ['Mampu', 'Menengah', 'Kurang Mampu']},
'Pertanian': {'mapel': ['biologi', 'geografi'], 'riasec': ['R', 'I'], 'eko_min': ['Mampu', 'Menengah', 'Kurang Mampu']},
'Peternakan': {'mapel': ['biologi', 'ekonomi_mapel'], 'riasec': ['R', 'E'], 'eko_min': ['Mampu', 'Menengah', 'Kurang Mampu']},
'Agribisnis': {'mapel': ['ekonomi_mapel', 'biologi'], 'riasec': ['E', 'R'], 'eko_min': ['Mampu', 'Menengah', 'Kurang Mampu']},
'Akuntansi': {'mapel': ['ekonomi_mapel', 'matematika'], 'riasec': ['C', 'E'], 'eko_min': ['Mampu', 'Menengah', 'Kurang Mampu']},
'Manajemen': {'mapel': ['ekonomi_mapel', 'sosiologi'], 'riasec': ['E', 'C'], 'eko_min': ['Mampu', 'Menengah', 'Kurang Mampu']},
'Ilmu Ekonomi': {'mapel': ['ekonomi_mapel', 'matematika', 'sejarah'], 'riasec': ['I', 'C'], 'eko_min': ['Mampu', 'Menengah', 'Kurang Mampu']},
'Ilmu Hukum': {'mapel': ['pkn', 'sosiologi', 'bahasa_indonesia'], 'riasec': ['E', 'S'], 'eko_min': ['Mampu', 'Menengah', 'Kurang Mampu']},
'Hubungan Internasional': {'mapel': ['bahasa_inggris', 'sejarah', 'pkn'], 'riasec': ['E', 'S'], 'eko_min': ['Mampu', 'Menengah']},
'Ilmu Komunikasi': {'mapel': ['bahasa_indonesia', 'sosiologi', 'bahasa_inggris'], 'riasec': ['E', 'A'], 'eko_min': ['Mampu', 'Menengah', 'Kurang Mampu']},
'Psikologi': {'mapel': ['sosiologi', 'bahasa_indonesia', 'biologi'], 'riasec': ['S', 'I'], 'eko_min': ['Mampu', 'Menengah', 'Kurang Mampu']},
'Administrasi Negara': {'mapel': ['pkn', 'sosiologi'], 'riasec': ['C', 'E'], 'eko_min': ['Mampu', 'Menengah', 'Kurang Mampu']},
'Desain Komunikasi Visual': {'mapel': ['seni_budaya', 'informatika'], 'riasec': ['A', 'E'], 'eko_min': ['Mampu', 'Menengah']},
'Arsitektur': {'mapel': ['seni_budaya', 'fisika', 'matematika'], 'riasec': ['A', 'I'], 'eko_min': ['Mampu', 'Menengah']},
'Sastra Inggris': {'mapel': ['bahasa_inggris', 'sejarah', 'seni_budaya'], 'riasec': ['A', 'S'], 'eko_min': ['Mampu', 'Menengah', 'Kurang Mampu']},
'Pendidikan Guru SD': {'mapel': ['pkn', 'bahasa_indonesia', 'seni_budaya'], 'riasec': ['S', 'C'], 'eko_min': ['Mampu', 'Menengah', 'Kurang Mampu']},
'Pendidikan Matematika': {'mapel': ['matematika', 'pkn'], 'riasec': ['S', 'I'], 'eko_min': ['Mampu', 'Menengah', 'Kurang Mampu']},
'Pendidikan Jasmani': {'mapel': ['pjok', 'biologi'], 'riasec': ['S', 'R'], 'eko_min': ['Mampu', 'Menengah', 'Kurang Mampu']},
'Pariwisata': {'mapel': ['bahasa_inggris', 'geografi', 'seni_budaya'], 'riasec': ['E', 'S'], 'eko_min': ['Mampu', 'Menengah', 'Kurang Mampu']}
}
siswa_id = 1
for jurusan, config in jurusan_config.items():
for _ in range(n_samples_per_class):
# 1. Base Scores Mapel (Acak Normal 75-84)
scores = {k: np.random.randint(75, 85) for k in [
'agama', 'pkn', 'bahasa_indonesia', 'bahasa_inggris', 'matematika',
'fisika', 'kimia', 'biologi', 'sejarah', 'geografi',
'ekonomi_mapel', 'sosiologi', 'seni_budaya', 'pjok', 'informatika'
]}
# Naikkan skor mapel utama jurusan tersebut
for m in config['mapel']:
scores[m] = np.random.randint(88, 98)
# 2. Base Scores Kuesioner RIASEC (Acak Normal 50-70)
riasec = {k: np.random.randint(50, 71) for k in ['R', 'I', 'A', 'S', 'E', 'C']}
# Naikkan skor RIASEC dominan jurusan tersebut
for r_key in config['riasec']:
riasec[r_key] = np.random.randint(85, 98)
# 3. Ekonomi Orang Tua
eko = np.random.choice(config['eko_min'])
# 4. Susun Baris Data
row = [f"SW{siswa_id:05d}", f"Siswa_{siswa_id}"] + \
list(scores.values()) + \
[riasec['R'], riasec['I'], riasec['A'], riasec['S'], riasec['E'], riasec['C']] + \
[eko, jurusan]
rows.append(row)
siswa_id += 1
columns = ['id_siswa', 'nama_lengkap',
'agama', 'pkn', 'bahasa_indonesia', 'bahasa_inggris', 'matematika',
'fisika', 'kimia', 'biologi', 'sejarah', 'geografi',
'ekonomi_mapel', 'sosiologi', 'seni_budaya', 'pjok', 'informatika',
'riasec_r', 'riasec_i', 'riasec_a', 'riasec_s', 'riasec_e', 'riasec_c',
'ekonomi_orang_tua', 'rekomendasi_jurusan']
# Acak urutan baris data agar tidak terurut per jurusan saat di training
df = pd.DataFrame(rows, columns=columns)
df = df.sample(frac=1, random_state=42).reset_index(drop=True)
return df
# %% [3] EKSEKUSI DAN SIMPAN DATA
# Mengambil 200 sampel per jurusan x 30 jurusan = 6.000 data latih
df_synthetic = generate_realistic_data(n_samples_per_class=200)
df_synthetic.to_csv('dataset_15mapel_bersih.csv', index=False)
print(f"Dataset berhasil dibuat dengan total {len(df_synthetic)} baris!")
print("Fitur baru: Skor RIASEC ditambahkan untuk 30 Jurusan.")
# %%

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,80 @@
# %% [1] IMPORT LIBRARIES & LOAD DATA
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
from sklearn.preprocessing import LabelEncoder
import joblib
# Mengatur tema visualisasi agar terlihat profesional untuk laporan TA
plt.style.use('seaborn-v0_8-whitegrid')
print("--- [STEP 2] EDA, Visualisasi, & Preprocessing (Integrasi RIASEC) ---")
df = pd.read_csv('dataset_15mapel_bersih.csv')
# %% [2] VISUALISASI 1: DISTRIBUSI KELAS (SEBELUM ENCODING)
print("\nMenampilkan Grafik Distribusi Jurusan...")
plt.figure(figsize=(12, 10))
# Membuat grafik batang horizontal agar 30 nama jurusan terbaca jelas
sns.countplot(y='rekomendasi_jurusan', data=df,
order=df['rekomendasi_jurusan'].value_counts().index,
palette='mako')
plt.title('Distribusi Jumlah Siswa per Jurusan (Memastikan Data Seimbang)', fontsize=16, fontweight='bold')
plt.xlabel('Jumlah Siswa', fontsize=12)
plt.ylabel('Jurusan', fontsize=12)
plt.tight_layout()
plt.show()
# %% [3] PREPROCESSING TAHAP 1: HAPUS KOLOM TEKS (IDENTITAS)
print("\nMelakukan Preprocessing Dasar...")
# Kolom kategori teks yang akan kita pertahankan untuk di-encode
kolom_kategori_penting = ['ekonomi_orang_tua', 'rekomendasi_jurusan']
# Hapus kolom id_siswa dan nama_lengkap karena mesin tidak belajar dari nama/ID
for col in df.select_dtypes(include=['object']).columns:
if col not in kolom_kategori_penting:
df = df.drop(columns=[col])
print(f"✅ Kolom '{col}' berhasil dibuang.")
df = df.dropna()
# %% [4] PREPROCESSING TAHAP 2: LABEL ENCODING
print("\nMelakukan Encoding (Mengubah Teks menjadi Angka)...")
encoders = {}
for col in kolom_kategori_penting:
if col in df.columns:
le = LabelEncoder()
df[col] = le.fit_transform(df[col])
encoders[col] = le
print(f"✅ Kamus '{col}' tersimpan. Jumlah kelas: {len(le.classes_)}")
# Simpan encoder untuk digunakan di web/aplikasi (sangat penting untuk decode di PHP Laravel)
joblib.dump(encoders, 'encoders_15mapel.pkl')
# %% [5] VISUALISASI 2: HEATMAP KORELASI (SETELAH ENCODING)
print("\nMenampilkan Heatmap Korelasi Antar Fitur...")
plt.figure(figsize=(22, 18)) # Ukuran diperbesar karena fiturnya banyak (21 fitur)
# Membuat korelasi pearson
corr_matrix = df.corr()
# Menampilkan Heatmap
sns.heatmap(corr_matrix, annot=True, fmt='.2f', cmap='coolwarm',
vmin=-1, vmax=1, square=True, linewidths=.5, annot_kws={"size": 8})
plt.title('Heatmap Korelasi: 15 Mapel, 6 Skor RIASEC, Ekonomi, dan Target Jurusan', fontsize=18, fontweight='bold')
plt.xticks(rotation=45, ha='right', fontsize=10)
plt.yticks(fontsize=10)
plt.tight_layout()
plt.show()
# %% [6] MENYIMPAN DAFTAR FITUR & DATASET BERSIH
print("\nMenyimpan urutan kolom (features) agar saat testing di Laravel tidak tertukar posisinya...")
feature_columns = [col for col in df.columns if col != 'rekomendasi_jurusan']
joblib.dump(feature_columns, 'feature_columns_15mapel.pkl')
print(f"✅ Urutan fitur tersimpan: {feature_columns}")
# Simpan data yang sudah berupa angka semua (termasuk 6 kolom riasec)
df.to_csv('dataset_siswa_siap_model.csv', index=False)
print("\nData siap latih berhasil disimpan sebagai 'dataset_siswa_siap_model.csv'!")
# %%

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,68 @@
# %% [1] IMPORT LIBRARIES & LOAD DATA
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report, accuracy_score
import joblib
print("--- [STEP 3] Pelatihan Model Random Forest 30 Jurusan ---")
print("Memuat dataset yang sudah diproses...")
df = pd.read_csv('dataset_siswa_siap_model.csv')
# %% [2] MEMISAHKAN FITUR DAN TARGET (DATA SPLIT)
print("Membagi fitur dan target...")
X = df.drop('rekomendasi_jurusan', axis=1)
y = df['rekomendasi_jurusan']
print("Membagi data menjadi data latih (80%) dan data uji (20%)...")
# Karena ada 30 kelas, stratify sangat wajib agar distribusi per kelas merata
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)
print(f"Jumlah Data Latih: {X_train.shape[0]} | Jumlah Data Uji: {X_test.shape[0]}")
# %% [3] TRAINING MODEL RANDOM FOREST (HYPERPARAMETER ANTI-OVERFIT)
print("\nMelatih model Random Forest...")
# Dengan data dimensi RIASEC, kita bisa membebaskan pohon sedikit lebih dalam
rf_model = RandomForestClassifier(
n_estimators=200, # 200 pohon sudah sangat cukup untuk 30 jurusan
max_depth=12, # Cegah menghafal terlalu dalam (Anti Overfitting)
min_samples_split=4, # Split akan terjadi jika minimal ada 4 sampel
min_samples_leaf=2, # Daun terakhir minimal berisi 2 sampel
max_features='sqrt',
class_weight='balanced',
random_state=42,
n_jobs=-1
)
rf_model.fit(X_train, y_train)
print("Proses pelatihan selesai!")
# %% [4] EVALUASI MODEL
print("\nMengevaluasi performa model pada data uji...")
y_pred = rf_model.predict(X_test)
akurasi = accuracy_score(y_test, y_pred)
print(f"\n======================================")
print(f"AKURASI MODEL 30 JURUSAN: {akurasi * 100:.2f}%")
print(f"======================================")
print("\nLaporan Detail Prediksi Keseluruhan:")
print(classification_report(y_test, y_pred))
# Cek Feature Importances (Membuktikan bahwa RIASEC akan memiliki bobot besar)
importances = rf_model.feature_importances_
feature_names = X.columns
feature_imp_df = pd.DataFrame({'Fitur': feature_names, 'Bobot': importances}).sort_values('Bobot', ascending=False)
plt.figure(figsize=(10, 8))
sns.barplot(x='Bobot', y='Fitur', data=feature_imp_df, palette='magma')
plt.title('Tingkat Kepentingan Fitur (Feature Importances)', fontsize=14)
plt.tight_layout()
plt.show()
# %% [5] SIMPAN MODEL
print("\nMenyimpan struktur model yang sudah berhasil dilatih...")
joblib.dump(rf_model, 'model_rekomendasi_15mapel.pkl')
print("Model berhasil diamankan ke dalam format 'model_rekomendasi_15mapel.pkl'!")
# %%

View File

@ -7,12 +7,15 @@
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {
//
->withMiddleware(function (Middleware $middleware) {
$middleware->alias([
'role' => \App\Http\Middleware\RoleMiddleware::class,
]);
})
->withExceptions(function (Exceptions $exceptions): void {
->withExceptions(function (Exceptions $exceptions) {
//
})->create();
})->create();

View File

@ -8,6 +8,7 @@
"require": {
"php": "^8.2",
"laravel/framework": "^12.0",
"laravel/sanctum": "^4.3",
"laravel/tinker": "^2.10.1"
},
"require-dev": {

65
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "c514d8f7b9fc5970bdd94287905ef584",
"content-hash": "ad91ab9bde70e3576f2b64dd11542bc4",
"packages": [
{
"name": "brick/math",
@ -1333,6 +1333,69 @@
},
"time": "2026-01-13T20:29:29+00:00"
},
{
"name": "laravel/sanctum",
"version": "v4.3.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/sanctum.git",
"reference": "e3b85d6e36ad00e5db2d1dcc27c81ffdf15cbf76"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/sanctum/zipball/e3b85d6e36ad00e5db2d1dcc27c81ffdf15cbf76",
"reference": "e3b85d6e36ad00e5db2d1dcc27c81ffdf15cbf76",
"shasum": ""
},
"require": {
"ext-json": "*",
"illuminate/console": "^11.0|^12.0|^13.0",
"illuminate/contracts": "^11.0|^12.0|^13.0",
"illuminate/database": "^11.0|^12.0|^13.0",
"illuminate/support": "^11.0|^12.0|^13.0",
"php": "^8.2",
"symfony/console": "^7.0|^8.0"
},
"require-dev": {
"mockery/mockery": "^1.6",
"orchestra/testbench": "^9.15|^10.8|^11.0",
"phpstan/phpstan": "^1.10"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Laravel\\Sanctum\\SanctumServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Laravel\\Sanctum\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.",
"keywords": [
"auth",
"laravel",
"sanctum"
],
"support": {
"issues": "https://github.com/laravel/sanctum/issues",
"source": "https://github.com/laravel/sanctum"
},
"time": "2026-02-07T17:19:31+00:00"
},
{
"name": "laravel/serializable-closure",
"version": "v2.0.8",

84
config/sanctum.php Normal file
View File

@ -0,0 +1,84 @@
<?php
use Laravel\Sanctum\Sanctum;
return [
/*
|--------------------------------------------------------------------------
| Stateful Domains
|--------------------------------------------------------------------------
|
| Requests from the following domains / hosts will receive stateful API
| authentication cookies. Typically, these should include your local
| and production domains which access your API via a frontend SPA.
|
*/
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
'%s%s',
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
Sanctum::currentApplicationUrlWithPort(),
// Sanctum::currentRequestHost(),
))),
/*
|--------------------------------------------------------------------------
| Sanctum Guards
|--------------------------------------------------------------------------
|
| This array contains the authentication guards that will be checked when
| Sanctum is trying to authenticate a request. If none of these guards
| are able to authenticate the request, Sanctum will use the bearer
| token that's present on an incoming request for authentication.
|
*/
'guard' => ['web'],
/*
|--------------------------------------------------------------------------
| Expiration Minutes
|--------------------------------------------------------------------------
|
| This value controls the number of minutes until an issued token will be
| considered expired. This will override any values set in the token's
| "expires_at" attribute, but first-party sessions are not affected.
|
*/
'expiration' => null,
/*
|--------------------------------------------------------------------------
| Token Prefix
|--------------------------------------------------------------------------
|
| Sanctum can prefix new tokens in order to take advantage of numerous
| security scanning initiatives maintained by open source platforms
| that notify developers if they commit tokens into repositories.
|
| See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning
|
*/
'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''),
/*
|--------------------------------------------------------------------------
| Sanctum Middleware
|--------------------------------------------------------------------------
|
| When authenticating your first-party SPA with Sanctum you may need to
| customize some of the middleware Sanctum uses while processing the
| request. You may change the middleware listed below as required.
|
*/
'middleware' => [
'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class,
'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class,
'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
],
];

View File

@ -14,9 +14,11 @@ public function up(): void
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('username')->unique();
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->string('role');
$table->rememberToken();
$table->timestamps();
});

View File

@ -0,0 +1,25 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('jurusans', function (Blueprint $table) {
$table->id(); // Mewakili Id_Jurusan
$table->string('nama_jurusan');
$table->string('rumpun');
$table->text('deskripsi')->nullable();
$table->text('prospek_karir')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('jurusans');
}
};

View File

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('siswas', function (Blueprint $table) {
$table->id(); // Primary Key Utama (BigInt)
$table->string('id_siswa')->unique();
$table->foreignId('user_id')->constrained('users')->onDelete('cascade');
$table->foreignId('jurusan_id')->constrained('jurusans')->onDelete('cascade');
$table->string('nama_lengkap');
$table->string('nisn')->unique();
$table->string('kelas');
$table->string('peminatan');
$table->string('minat_bakat')->nullable();
$table->string('ekonomi')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('siswas');
}
};

View File

@ -0,0 +1,23 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('gurus', function (Blueprint $table) {
$table->id();
$table->string('id_guru')->unique(); // Tambahkan ID Bisnis
$table->string('nama_guru');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('gurus');
}
};

View File

@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('alumnis', function (Blueprint $table) {
$table->id();
$table->string('id_alumni')->unique(); // Tambahkan ID Bisnis
$table->string('nama_alumni');
$table->string('perguruan_tinggi');
$table->string('jurusan_diambil');
$table->text('profil_nilai')->nullable();
$table->text('profil_minat')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('alumnis');
}
};

View File

@ -0,0 +1,33 @@
<?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('personal_access_tokens', function (Blueprint $table) {
$table->id();
$table->morphs('tokenable');
$table->text('name');
$table->string('token', 64)->unique();
$table->text('abilities')->nullable();
$table->timestamp('last_used_at')->nullable();
$table->timestamp('expires_at')->nullable()->index();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('personal_access_tokens');
}
};

View File

@ -0,0 +1,44 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void {
Schema::create('riwayat_rekomendasis', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete(); // Hilangkan komentar jika sudah pakai login Auth
// Kolom Profil
$table->integer('gaji_ortu');
$table->integer('ekonomi_orang_tua'); // 0: Kurang, 1: Menengah, 2: Mampu
$table->integer('minat_bakat'); // 0-3 sesuai dataset
// 15 Nilai Mata Pelajaran
$table->float('agama');
$table->float('pkn');
$table->float('bahasa_indonesia');
$table->float('bahasa_inggris');
$table->float('matematika');
$table->float('fisika');
$table->float('kimia');
$table->float('biologi');
$table->float('sejarah');
$table->float('geografi');
$table->float('ekonomi_mapel');
$table->float('sosiologi');
$table->float('seni_budaya');
$table->float('pjok');
$table->float('informatika');
// Hasil Akhir
$table->string('hasil_rekomendasi_jurusan')->nullable();
$table->timestamps();
});
}
public function down(): void {
Schema::dropIfExists('riwayat_rekomendasis');
}
};

View File

@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('prospek_kerjas', function (Blueprint $table) {
$table->id();
// Menghubungkan prospek kerja ke ID jurusan tertentu
$table->foreignId('jurusan_id')->constrained('jurusans')->onDelete('cascade');
$table->string('nama_profesi');
$table->string('range_gaji')->nullable(); // Contoh: Rp 7.000.000 - Rp 15.000.000
$table->text('deskripsi_pekerjaan')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('prospek_kerjas');
}
};

View File

@ -0,0 +1,54 @@
<?php
namespace Database\Seeders;
use App\Models\Alumni;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
class AlumniSeeder extends Seeder
{
public function run(): void
{
// Bersihkan data lama agar ID reset ke AL01
DB::statement('SET FOREIGN_KEY_CHECKS=0;');
Alumni::truncate();
DB::statement('SET FOREIGN_KEY_CHECKS=1;');
$alumnis = [
[
'nama_alumni' => 'Andi Wijaya',
'perguruan_tinggi' => 'Universitas Indonesia',
'jurusan_diambil' => 'Ilmu Komputer',
'profil_nilai' => 'Matematika: 90, Fisika: 85',
'profil_minat' => 'Teknologi dan Logika'
],
[
'nama_alumni' => 'Bunga Citra',
'perguruan_tinggi' => 'Universitas Gadjah Mada',
'jurusan_diambil' => 'Akuntansi',
'profil_nilai' => 'Ekonomi: 95, Matematika: 88',
'profil_minat' => 'Keuangan dan Analisis'
],
[
'nama_alumni' => 'Candra Kirana',
'perguruan_tinggi' => 'Institut Teknologi Bandung',
'jurusan_diambil' => 'Teknik Informatika',
'profil_nilai' => 'Informatika: 92, Inggris: 85',
'profil_minat' => 'Programming'
],
[
'nama_alumni' => 'Dedi Setiadi',
'perguruan_tinggi' => 'Universitas Padjadjaran',
'jurusan_diambil' => 'Ilmu Komunikasi',
'profil_nilai' => 'Sosiologi: 88, B.Indonesia: 90',
'profil_minat' => 'Public Speaking'
],
];
foreach ($alumnis as $a) {
Alumni::create($a);
}
}
}

View File

@ -3,23 +3,31 @@
namespace Database\Seeders;
use App\Models\User;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
use WithoutModelEvents;
/**
* Seed the application's database.
*/
public function run(): void
{
// User::factory(10)->create();
// 1. Buat 4 User untuk Siswa dengan menyertakan kolom 'name'
for ($i = 1; $i <= 4; $i++) {
User::create([
'name' => 'Siswa Contoh ' . $i, // Tambahkan baris ini
'username' => 'siswa' . $i,
'email' => 'siswa' . $i . '@example.com',
'password' => bcrypt('password'),
'role' => 'siswa'
]);
}
User::factory()->create([
'name' => 'Test User',
'email' => 'test@example.com',
// 2. Panggil Seeder lainnya
$this->call([
JurusanSeeder::class,
GuruSeeder::class,
AlumniSeeder::class,
SiswaSeeder::class,
UserSeeder::class,
ProspekKerjaSeeder::class,
]);
}
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace Database\Seeders;
use App\Models\Guru;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
class GuruSeeder extends Seeder
{
public function run(): void
{
// Bersihkan data lama agar ID mulai dari GR01 lagi
DB::statement('SET FOREIGN_KEY_CHECKS=0;');
Guru::truncate();
DB::statement('SET FOREIGN_KEY_CHECKS=1;');
$gurus = [
['nama_guru' => 'Budi Santoso, S.Pd'],
['nama_guru' => 'Siti Aminah, M.Pd'],
['nama_guru' => 'Hendra Wijaya, S.Kom'],
['nama_guru' => 'Rina Kartika, M.Si'],
];
foreach ($gurus as $g) {
// Menggunakan Guru::create agar fungsi boot() di model terpanggil otomatis
Guru::create($g);
}
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace Database\Seeders;
use App\Models\Jurusan;
use Illuminate\Database\Seeder;
class JurusanSeeder extends Seeder
{
public function run(): void
{
$data = [
[
'nama_jurusan' => 'Teknik Informatika',
'rumpun' => 'Saintek',
'deskripsi' => 'Fokus pada pengembangan perangkat lunak dan sistem komputer.',
'prospek_karir' => 'Software Engineer, Data Scientist, Web Developer'
],
[
'nama_jurusan' => 'Sistem Informasi',
'rumpun' => 'Saintek',
'deskripsi' => 'Menggabungkan teknologi informasi dengan manajemen bisnis.',
'prospek_karir' => 'IT Consultant, Systems Analyst, Project Manager'
],
[
'nama_jurusan' => 'Akuntansi',
'rumpun' => 'Soshum',
'deskripsi' => 'Mempelajari pencatatan dan pelaporan transaksi keuangan.',
'prospek_karir' => 'Auditor, Tax Consultant, Financial Analyst'
],
[
'nama_jurusan' => 'Ilmu Komunikasi',
'rumpun' => 'Soshum',
'deskripsi' => 'Mempelajari proses penyampaian pesan secara efektif.',
'prospek_karir' => 'Public Relations, Journalist, Content Creator'
],
];
foreach ($data as $j) {
Jurusan::create($j);
}
}
}

View File

@ -0,0 +1,116 @@
<?php
namespace Database\Seeders;
use App\Models\Jurusan;
use App\Models\ProspekKerja;
use Illuminate\Database\Seeder;
class ProspekKerjaSeeder extends Seeder
{
public function run(): void
{
$semuaJurusan = [
[
'nama_jurusan' => 'Teknik Informatika / Ilmu Komputer',
'rumpun' => 'Saintek',
'deskripsi' => 'Fokus pada pengembangan perangkat lunak, kecerdasan buatan, keamanan siber, dan analisis data.',
'prospek' => [
['nama_profesi' => 'Software Engineer', 'range_gaji' => 'Rp 8.000.000 - Rp 25.000.000', 'deskripsi_pekerjaan' => 'Membangun, mengembangkan, dan memelihara aplikasi atau sistem perangkat lunak.'],
['nama_profesi' => 'Data Scientist', 'range_gaji' => 'Rp 10.000.000 - Rp 30.000.000', 'deskripsi_pekerjaan' => 'Menganalisis data besar (Big Data) untuk menemukan pola dan memberikan wawasan bagi strategi bisnis.'],
['nama_profesi' => 'Cyber Security Specialist', 'range_gaji' => 'Rp 9.000.000 - Rp 25.000.000', 'deskripsi_pekerjaan' => 'Melindungi sistem jaringan dan data perusahaan dari ancaman peretasan dan kebocoran data.'],
['nama_profesi' => 'UI/UX Designer', 'range_gaji' => 'Rp 7.000.000 - Rp 18.000.000', 'deskripsi_pekerjaan' => 'Merancang antarmuka pengguna dan pengalaman pengguna yang optimal pada website atau aplikasi.'],
]
],
[
'nama_jurusan' => 'Kedokteran / Farmasi',
'rumpun' => 'Saintek',
'deskripsi' => 'Mempelajari ilmu medis, diagnosa penyakit, penanganan pasien, dan formulasi obat-obatan.',
'prospek' => [
['nama_profesi' => 'Dokter Umum / Spesialis', 'range_gaji' => 'Rp 10.000.000 - Rp 40.000.000', 'deskripsi_pekerjaan' => 'Mendiagnosis, merawat pasien, dan memberikan tindakan medis di rumah sakit atau klinik.'],
['nama_profesi' => 'Apoteker / Ahli Farmasi', 'range_gaji' => 'Rp 7.000.000 - Rp 15.000.000', 'deskripsi_pekerjaan' => 'Meracik obat, meneliti efektivitas obat baru, dan memberikan edukasi penggunaan obat kepada pasien.'],
['nama_profesi' => 'Peneliti Medis', 'range_gaji' => 'Rp 8.000.000 - Rp 20.000.000', 'deskripsi_pekerjaan' => 'Melakukan eksperimen klinis untuk menemukan vaksin atau pengobatan baru.'],
['nama_profesi' => 'Manajemen Rumah Sakit', 'range_gaji' => 'Rp 9.000.000 - Rp 25.000.000', 'deskripsi_pekerjaan' => 'Mengatur operasional, administrasi, dan kebijakan strategis di fasilitas pelayanan kesehatan.'],
]
],
[
'nama_jurusan' => 'Hukum / Hubungan Internasional',
'rumpun' => 'Soshum',
'deskripsi' => 'Mempelajari sistem peradilan, regulasi negara, serta diplomasi dan politik antar negara.',
'prospek' => [
['nama_profesi' => 'Pengacara / Advokat', 'range_gaji' => 'Rp 8.000.000 - Rp 50.000.000', 'deskripsi_pekerjaan' => 'Memberikan pendampingan hukum dan mewakili klien di pengadilan.'],
['nama_profesi' => 'Hakim atau Jaksa', 'range_gaji' => 'Rp 15.000.000 - Rp 35.000.000', 'deskripsi_pekerjaan' => 'Menjalankan proses penuntutan pidana dan memberikan putusan hukum yang adil di pengadilan.'],
['nama_profesi' => 'Diplomat', 'range_gaji' => 'Rp 12.000.000 - Rp 30.000.000', 'deskripsi_pekerjaan' => 'Mewakili kepentingan negara dalam forum internasional dan menjaga hubungan bilateral.'],
['nama_profesi' => 'Legal Officer Perusahaan', 'range_gaji' => 'Rp 7.000.000 - Rp 20.000.000', 'deskripsi_pekerjaan' => 'Memastikan seluruh kegiatan operasional perusahaan berjalan sesuai dengan regulasi dan undang-undang yang berlaku.'],
]
],
[
'nama_jurusan' => 'Ekonomi / Manajemen Bisnis',
'rumpun' => 'Soshum',
'deskripsi' => 'Mempelajari pengelolaan sumber daya, keuangan, strategi pemasaran, dan operasional bisnis.',
'prospek' => [
['nama_profesi' => 'Manajer Keuangan', 'range_gaji' => 'Rp 10.000.000 - Rp 30.000.000', 'deskripsi_pekerjaan' => 'Mengelola arus kas, investasi, dan menyusun perencanaan keuangan instansi atau perusahaan.'],
['nama_profesi' => 'Analis Bisnis', 'range_gaji' => 'Rp 8.000.000 - Rp 22.000.000', 'deskripsi_pekerjaan' => 'Mengevaluasi model bisnis perusahaan dan memberikan rekomendasi peningkatan efisiensi melalui data.'],
['nama_profesi' => 'Akuntan Publik', 'range_gaji' => 'Rp 7.000.000 - Rp 25.000.000', 'deskripsi_pekerjaan' => 'Melakukan audit pembukuan keuangan perusahaan agar sesuai dengan standar transparansi.'],
['nama_profesi' => 'Business Development', 'range_gaji' => 'Rp 7.000.000 - Rp 18.000.000', 'deskripsi_pekerjaan' => 'Mencari peluang kemitraan baru, klien potensial, dan strategi ekspansi pasar.'],
]
],
[
'nama_jurusan' => 'Teknik Sipil / Arsitektur',
'rumpun' => 'Saintek',
'deskripsi' => 'Fokus pada perancangan, pembangunan, estetika, dan pemeliharaan infrastruktur dan bangunan.',
'prospek' => [
['nama_profesi' => 'Manajer Proyek Konstruksi', 'range_gaji' => 'Rp 10.000.000 - Rp 30.000.000', 'deskripsi_pekerjaan' => 'Mengawasi jalannya proyek konstruksi di lapangan agar selesai sesuai jadwal, kualitas, dan anggaran.'],
['nama_profesi' => 'Arsitek Profesional', 'range_gaji' => 'Rp 7.000.000 - Rp 25.000.000', 'deskripsi_pekerjaan' => 'Merancang estetika dan fungsionalitas bangunan, membuat cetak biru, serta visualisasi 3D.'],
['nama_profesi' => 'Insinyur Struktur', 'range_gaji' => 'Rp 8.000.000 - Rp 20.000.000', 'deskripsi_pekerjaan' => 'Melakukan perhitungan material untuk memastikan keamanan dan kestabilan beban bangunan atau jembatan.'],
['nama_profesi' => 'Surveyor dan Drafter', 'range_gaji' => 'Rp 5.000.000 - Rp 12.000.000', 'deskripsi_pekerjaan' => 'Mengukur kontur topografi lahan serta memindahkan sketsa desain menjadi gambar teknis presisi.'],
]
],
[
'nama_jurusan' => 'Psikologi / Ilmu Sosial',
'rumpun' => 'Soshum',
'deskripsi' => 'Mempelajari perilaku manusia, proses mental, serta interaksi dan dinamika di masyarakat.',
'prospek' => [
['nama_profesi' => 'Manajer HRD (Human Resources)', 'range_gaji' => 'Rp 8.000.000 - Rp 25.000.000', 'deskripsi_pekerjaan' => 'Mengelola proses rekrutmen, pelatihan, evaluasi, dan kesejahteraan karyawan dalam suatu perusahaan.'],
['nama_profesi' => 'Psikolog Klinis / Konselor', 'range_gaji' => 'Rp 7.000.000 - Rp 20.000.000', 'deskripsi_pekerjaan' => 'Mendiagnosis dan memberikan terapi untuk gangguan kesehatan mental atau masalah emosional klien.'],
['nama_profesi' => 'Peneliti Sosial', 'range_gaji' => 'Rp 6.000.000 - Rp 15.000.000', 'deskripsi_pekerjaan' => 'Merancang dan melakukan riset tentang tren sosial, kebijakan publik, atau perilaku konsumen masyarakat.'],
['nama_profesi' => 'Konselor Pendidikan', 'range_gaji' => 'Rp 5.000.000 - Rp 12.000.000', 'deskripsi_pekerjaan' => 'Membantu siswa merencanakan masa depan akademik, memberikan motivasi, serta menangani masalah perilaku.'],
]
],
[
'nama_jurusan' => 'Seni Rupa / Desain Komunikasi Visual',
'rumpun' => 'Soshum',
'deskripsi' => 'Fokus pada penyampaian pesan komunikasi melalui elemen visual, estetika rupa, dan desain kreatif.',
'prospek' => [
['nama_profesi' => 'Art Director', 'range_gaji' => 'Rp 10.000.000 - Rp 25.000.000', 'deskripsi_pekerjaan' => 'Memimpin tim kreatif dan menentukan konsep visual utama untuk kampanye iklan, film, atau majalah.'],
['nama_profesi' => 'Graphic Designer', 'range_gaji' => 'Rp 5.000.000 - Rp 15.000.000', 'deskripsi_pekerjaan' => 'Menciptakan desain identitas visual seperti logo, kemasan produk, dan materi promosi untuk brand.'],
['nama_profesi' => 'Animator / 3D Artist', 'range_gaji' => 'Rp 7.000.000 - Rp 20.000.000', 'deskripsi_pekerjaan' => 'Membuat gambar bergerak (animasi), visual effects (VFX), atau model 3D untuk industri film dan video game.'],
['nama_profesi' => 'Fotografer / Videografer Komersial', 'range_gaji' => 'Rp 6.000.000 - Rp 20.000.000', 'deskripsi_pekerjaan' => 'Mengambil dan menyunting gambar atau video untuk kebutuhan editorial, komersial, atau dokumentasi acara.'],
]
]
];
foreach ($semuaJurusan as $data) {
$jurusan = Jurusan::firstOrCreate(
['nama_jurusan' => $data['nama_jurusan']],
[
'rumpun' => $data['rumpun'],
'deskripsi' => $data['deskripsi']
]
);
foreach ($data['prospek'] as $prospekData) {
ProspekKerja::firstOrCreate(
[
'jurusan_id' => $jurusan->id,
'nama_profesi' => $prospekData['nama_profesi']
],
[
'range_gaji' => $prospekData['range_gaji'],
'deskripsi_pekerjaan' => $prospekData['deskripsi_pekerjaan']
]
);
}
}
}
}

View File

@ -0,0 +1,85 @@
<?php
namespace Database\Seeders;
use App\Models\Siswa;
use App\Models\User;
use App\Models\Jurusan;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
class SiswaSeeder extends Seeder
{
public function run(): void
{
// 1. Bersihkan data lama agar tidak terjadi error Unique NISN saat seeding ulang
// Menggunakan truncate atau delete agar ID auto-increment mulai dari awal lagi jika perlu
DB::statement('SET FOREIGN_KEY_CHECKS=0;');
Siswa::truncate();
DB::statement('SET FOREIGN_KEY_CHECKS=1;');
// 2. Ambil semua user dengan role siswa dan semua jurusan
$users = User::where('role', 'siswa')->get();
$jurusans = Jurusan::all();
// Cek jika data master (User & Jurusan) tersedia
if ($users->isEmpty() || $jurusans->isEmpty()) {
$this->command->error("Data User atau Jurusan kosong! Silahkan jalankan UserSeeder dan JurusanSeeder terlebih dahulu.");
return;
}
$siswaData = [
[
'nama_lengkap' => 'Galang Sefian Adji',
'nisn' => '12345678912', // Sesuaikan dengan yang ada di gambar UI Anda
'kelas' => 'XII-IPA-1',
'peminatan' => 'Saintek',
'minat_bakat' => 'Programming',
'ekonomi' => 'Mampu'
],
[
'nama_lengkap' => 'Eka Putri',
'nisn' => '1234567891',
'kelas' => 'XII-IPS-2',
'peminatan' => 'Soshum',
'minat_bakat' => 'Menulis',
'ekonomi' => 'Menengah'
],
[
'nama_lengkap' => 'Fajar Ramadhan',
'nisn' => '1234567892',
'kelas' => 'XII-IPA-2',
'peminatan' => 'Saintek',
'minat_bakat' => 'Robotik',
'ekonomi' => 'Mampu'
],
[
'nama_lengkap' => 'alex',
'nisn' => '1243254354',
'kelas' => 'XII-IPA-1',
'peminatan' => 'Saintek',
'minat_bakat' => 'Musik',
'ekonomi' => 'Mampu'
],
];
foreach ($siswaData as $index => $data) {
if (isset($users[$index])) {
// Menggunakan Eloquent Create agar fungsi boot() di model Siswa terpanggil
// Jangan memasukkan 'id_siswa' secara manual di sini, biarkan model yang membuatnya
Siswa::create([
'user_id' => $users[$index]->id,
'jurusan_id' => $jurusans->random()->id,
'nama_lengkap' => $data['nama_lengkap'],
'nisn' => $data['nisn'],
'kelas' => $data['kelas'],
'peminatan' => $data['peminatan'],
'minat_bakat' => $data['minat_bakat'],
'ekonomi' => $data['ekonomi'],
]);
}
}
$this->command->info("Seeder Siswa berhasil dijalankan dengan ID otomatis.");
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
class UserSeeder extends Seeder
{
public function run()
{
// Membuat Admin
User::create([
'name' => 'Administrator',
'username' => 'admin',
'email' => 'admin@mail.com',
'password' => Hash::make('password123'),
'role' => 'admin', // Sesuai dengan role di AuthController
]);
// Membuat Siswa
User::create([
'name' => 'Siswa Budi',
'username' => 'siswa01',
'email' => 'budi@mail.com',
'password' => Hash::make('password123'),
'role' => 'siswa',
]);
}
}

181
flask_api/app.py Normal file
View File

@ -0,0 +1,181 @@
import os
import traceback
from flask import Flask, request, jsonify
from flask_cors import CORS
import joblib
import pandas as pd
app = Flask(__name__)
CORS(app)
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
MODEL_PATH = os.path.join(BASE_DIR, 'model_rekomendasi_15mapel.pkl')
ENCODER_PATH = os.path.join(BASE_DIR, 'encoders_15mapel.pkl')
FEATURE_COLUMNS_PATH = os.path.join(BASE_DIR, 'feature_columns_15mapel.pkl')
model = None
encoders = None
feature_columns = None
def load_artifacts():
global model, encoders, feature_columns
model = joblib.load(MODEL_PATH)
encoders = joblib.load(ENCODER_PATH)
feature_columns = joblib.load(FEATURE_COLUMNS_PATH)
if not isinstance(feature_columns, list):
raise ValueError("feature_columns_15mapel.pkl harus berisi list nama kolom.")
required_encoder_keys = ['ekonomi_orang_tua', 'rekomendasi_jurusan']
for key in required_encoder_keys:
if key not in encoders:
raise KeyError(f"Encoder '{key}' tidak ditemukan pada encoders_15mapel.pkl")
def safe_float(value, default=0.0):
try:
if value is None or value == '':
return float(default)
return float(value)
except (ValueError, TypeError):
return float(default)
def validate_required_features(data):
if not isinstance(data, dict):
return False, "Payload JSON tidak valid."
missing = []
for col in feature_columns:
if col == 'ekonomi_orang_tua':
continue
if col not in data:
missing.append(col)
if 'ekonomi_orang_tua' not in data:
missing.append('ekonomi_orang_tua')
if missing:
return False, f"Fitur berikut belum dikirim: {', '.join(missing)}"
return True, None
try:
load_artifacts()
print("✅ API Flask siap. Model, encoder, dan feature columns berhasil dimuat.")
except Exception as e:
print("❌ Gagal memuat aset machine learning.")
print(str(e))
@app.route('/', methods=['GET'])
def home():
return jsonify({
'success': True,
'message': 'Flask API Sistem Rekomendasi Jurusan aktif.'
})
@app.route('/health', methods=['GET'])
def health():
artifacts_ready = all([
model is not None,
encoders is not None,
feature_columns is not None
])
return jsonify({
'success': artifacts_ready,
'model_loaded': model is not None,
'encoders_loaded': encoders is not None,
'feature_columns_loaded': feature_columns is not None
}), 200 if artifacts_ready else 500
@app.route('/predict', methods=['POST'])
def predict():
global model, encoders, feature_columns
try:
if model is None or encoders is None or feature_columns is None:
return jsonify({
'success': False,
'message': 'Aset model belum berhasil dimuat. Periksa file .pkl dan path-nya.'
}), 500
data = request.get_json(silent=True)
if data is None:
return jsonify({
'success': False,
'message': 'Request harus berformat JSON.'
}), 400
is_valid, error_message = validate_required_features(data)
if not is_valid:
return jsonify({
'success': False,
'message': error_message
}), 400
eko_text = str(data.get('ekonomi_orang_tua', 'Mampu')).strip()
ekonomi_classes = list(encoders['ekonomi_orang_tua'].classes_)
if eko_text not in ekonomi_classes:
return jsonify({
'success': False,
'message': f"Nilai ekonomi_orang_tua tidak valid. Gunakan salah satu: {', '.join(ekonomi_classes)}"
}), 400
encoded_eko = encoders['ekonomi_orang_tua'].transform([eko_text])[0]
input_dict = {}
for col in feature_columns:
if col == 'ekonomi_orang_tua':
input_dict[col] = encoded_eko
else:
input_dict[col] = safe_float(data.get(col), 0)
df_input = pd.DataFrame([input_dict], columns=feature_columns)
pred_idx = model.predict(df_input)[0]
nama_jurusan = encoders['rekomendasi_jurusan'].inverse_transform([pred_idx])[0]
probabilities = None
if hasattr(model, 'predict_proba'):
try:
proba = model.predict_proba(df_input)[0]
class_labels = encoders['rekomendasi_jurusan'].inverse_transform(model.classes_)
probabilities = [
{
'jurusan': str(label),
'probabilitas': round(float(score), 4)
}
for label, score in sorted(
zip(class_labels, proba),
key=lambda x: x[1],
reverse=True
)
]
except Exception:
probabilities = None
return jsonify({
'success': True,
'prediksi_jurusan': str(nama_jurusan),
'top_predictions': probabilities
}), 200
except Exception as e:
traceback.print_exc()
return jsonify({
'success': False,
'message': f'Terjadi kesalahan saat melakukan prediksi: {str(e)}'
}), 500
if __name__ == '__main__':
app.run(host='127.0.0.1', port=5000, debug=True)

Binary file not shown.

2421
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

211
public/css/admin.css Normal file
View File

@ -0,0 +1,211 @@
/* ================================
THEME SMAN 4 JEMBER - MODERN DASHBOARD
================================ */
:root {
--primary-green: #2D7A47; /* Hijau SMAN 4 */
--dark-green: #1B4D2E;
--accent-yellow: #FFD700; /* Kuning Emas Logo */
--bg-light: #f8fafc;
--white: #ffffff;
--sidebar-width: 260px;
}
body {
background-color: var(--bg-light);
font-family: 'Figtree', sans-serif;
margin: 0;
overflow-x: hidden;
}
/* Layout Container */
.admin-wrapper {
display: flex;
min-height: 100vh;
}
/* --- PERBAIKAN SIDEBAR --- */
.sidebar-modern {
width: var(--sidebar-width);
background-color: var(--primary-green);
color: var(--white);
display: flex;
flex-direction: column;
position: fixed;
height: 100vh; /* Mengunci tinggi sidebar sesuai tinggi layar */
box-shadow: 4px 0 10px rgba(0,0,0,0.1);
z-index: 1000;
}
.sidebar-header {
padding: 30px 20px;
text-align: center;
border-bottom: 1px solid rgba(255,255,255,0.1);
flex-shrink: 0; /* Header tidak akan mengecil saat di-scroll */
}
.sidebar-header img {
width: 60px;
margin-bottom: 10px;
}
/* Area Menu dengan Fitur Scroll */
.sidebar-menu {
padding: 20px 15px;
flex-grow: 1;
overflow-y: auto; /* Mengaktifkan scroll vertikal */
scrollbar-width: thin; /* Untuk Firefox */
scrollbar-color: rgba(255,255,255,0.2) transparent;
}
/* Custom Scrollbar untuk Chrome/Edge/Safari */
.sidebar-menu::-webkit-scrollbar {
width: 6px;
}
.sidebar-menu::-webkit-scrollbar-thumb {
background-color: rgba(255,255,255,0.2);
border-radius: 10px;
}
.sidebar-menu::-webkit-scrollbar-track {
background: transparent;
}
.menu-label {
font-size: 11px;
text-transform: uppercase;
font-weight: 700;
color: rgba(255,255,255,0.5);
margin: 20px 0 10px 10px;
letter-spacing: 1px;
}
.nav-item {
display: flex;
align-items: center;
padding: 12px 15px;
color: var(--white);
text-decoration: none;
border-radius: 10px;
margin-bottom: 5px;
transition: all 0.3s;
font-weight: 500;
}
.nav-item i {
width: 25px;
font-size: 18px;
margin-right: 10px;
}
.nav-item:hover {
background-color: rgba(255,255,255,0.1);
color: var(--accent-yellow);
}
.nav-item.active {
background-color: var(--white);
color: var(--primary-green);
font-weight: 700;
}
/* Main Content Area */
.main-content {
margin-left: var(--sidebar-width);
flex-grow: 1;
display: flex;
flex-direction: column;
min-width: 0; /* Mencegah overflow pada elemen flex */
}
/* Topbar */
.topbar {
height: 70px;
background: var(--white);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 30px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
position: sticky;
top: 0;
z-index: 999;
}
.search-bar {
background: #f1f5f9;
border-radius: 10px;
padding: 8px 15px;
width: 350px;
display: flex;
align-items: center;
}
.search-bar input {
border: none;
background: transparent;
margin-left: 10px;
outline: none;
width: 100%;
}
/* Stats Cards */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 25px;
padding: 30px;
}
.stat-card {
background: var(--white);
padding: 25px;
border-radius: 20px;
box-shadow: 0 4px 6px rgba(0,0,0,0.02);
display: flex;
align-items: center;
justify-content: space-between;
border: 1px solid #f1f5f9;
}
.stat-info h3 {
margin: 0;
font-size: 28px;
font-weight: 800;
color: #1e293b;
}
.stat-info p {
margin: 0;
color: #64748b;
font-size: 14px;
font-weight: 600;
}
.stat-icon {
width: 55px;
height: 55px;
border-radius: 15px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
.icon-siswa { background: #eef2ff; color: #4f46e5; }
.icon-rekom { background: #ecfdf5; color: var(--primary-green); }
.icon-akurasi { background: #faf5ff; color: #9333ea; }
/* Dashboard Section */
.dashboard-section {
padding: 0 30px 30px 30px;
}
.chart-container {
background: var(--white);
border-radius: 20px;
padding: 25px;
border: 1px solid #f1f5f9;
min-height: 400px;
}

107
public/css/auth.css Normal file
View File

@ -0,0 +1,107 @@
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;700;800&display=swap');
:root {
--asgard-green-dark: #064e3b;
--asgard-green-bright: #10b981;
--asgard-lime: #a3e635;
}
body.auth-page {
background-color: #f0fdf4;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
font-family: 'Plus Jakarta Sans', sans-serif;
margin: 0;
}
/* Container Utama Split Screen */
.login-container {
width: 100%;
max-width: 1100px;
height: 700px;
background: white;
display: flex;
border-radius: 3rem;
overflow: hidden;
box-shadow: 0 50px 100px -20px rgba(6, 78, 59, 0.15);
}
/* Sisi Kiri: Visual Identity */
.login-visual {
flex: 1;
background: linear-gradient(135deg, var(--asgard-green-dark) 0%, #065f46 100%);
display: none;
flex-direction: column;
justify-content: center;
padding: 60px;
position: relative;
color: white;
}
@media (min-width: 768px) {
.login-visual { display: flex; }
}
.visual-pattern {
position: absolute;
top: 0; left: 0; width: 100%; height: 100%;
opacity: 0.1;
background-image: radial-gradient(#fff 1px, transparent 1px);
background-size: 30px 30px;
}
/* Sisi Kanan: Form */
.login-form-area {
flex: 1;
padding: 60px;
display: flex;
flex-direction: column;
justify-content: center;
}
.input-modern {
background: #f8fafc;
border: 2px solid #f1f5f9;
border-radius: 1.2rem;
padding: 1rem 1.5rem;
font-weight: 600;
font-size: 0.875rem;
transition: all 0.3s ease;
}
.input-modern:focus {
background: white;
border-color: var(--asgard-green-bright);
outline: none;
box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.1);
}
.btn-login {
background: var(--asgard-green-dark);
color: white;
padding: 1rem;
border-radius: 1.2rem;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.1em;
font-size: 0.75rem;
transition: all 0.3s ease;
}
.btn-login:hover {
background: #000;
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(6, 78, 59, 0.2);
}
.badge-ai {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
padding: 0.5rem 1rem;
border-radius: 50px;
font-size: 0.65rem;
font-weight: 800;
letter-spacing: 0.2em;
}

155
public/css/fitur-admin.css Normal file
View File

@ -0,0 +1,155 @@
.table-container-modern {
border: 1px solid #e2e8f0;
border-radius: 12px;
overflow: hidden;
background-color: #ffffff;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.table-modern {
width: 100%;
border-collapse: collapse;
margin-bottom: 0 !important;
}
.table-modern thead th {
background-color: #f8fafc;
color: #64748b;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 1px;
font-weight: 700;
padding: 15px 20px;
border-bottom: 2px solid #e2e8f0;
border-right: 1px solid #e2e8f0; /* Garis antar kolom */
}
.table-modern thead th:last-child {
border-right: none;
}
.table-modern tbody td {
padding: 15px 20px;
border-bottom: 1px solid #f1f5f9;
vertical-align: middle;
border-right: 1px solid #f1f5f9; /* Garis antar kolom */
}
.table-modern tbody td:last-child {
border-right: none;
}
/* 2. Opsi Tombol Aksi (Edit & Hapus) */
.opsi-wrapper {
display: flex;
gap: 8px;
justify-content: center;
align-items: center;
}
.btn-action {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
border: none;
color: #ffffff;
transition: transform 0.2s, opacity 0.2s;
}
.btn-action:hover {
transform: scale(1.1);
opacity: 0.9;
}
.btn-edit { background-color: #4f46e5; }
.btn-delete { background-color: #ef4444; }
/* 3. Pagination (Menyatu di bawah tabel) */
.pagination-container {
padding: 15px 20px;
display: flex;
justify-content: space-between;
align-items: center;
background-color: #ffffff;
border-top: 1px solid #e2e8f0;
}
.page-link-custom {
padding: 6px 14px;
border: 1px solid #e2e8f0;
background-color: #ffffff;
border-radius: 6px;
color: #64748b;
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
}
.page-link-custom.active {
background-color: #007bff;
color: #ffffff;
border-color: #007bff;
}
.page-link-custom:disabled {
background-color: #f8fafc;
cursor: not-allowed;
opacity: 0.5;
}
/* 4. Form Styling (Satu Baris Satu Input) */
.form-group-custom {
margin-bottom: 1.5rem;
}
.form-group-custom label {
font-size: 13px;
font-weight: 700;
color: #475569;
margin-bottom: 8px;
display: block;
}
.input-custom {
border-radius: 10px !important;
border: 1px solid #e2e8f0 !important;
padding: 12px 15px !important;
font-size: 14px;
width: 100%;
}
.input-custom:focus {
border-color: var(--primary-green) !important;
box-shadow: 0 0 0 3px rgba(45, 122, 71, 0.1) !important;
outline: none;
}
/* Badge Rumpun Jurusan */
.badge-saintek {
background-color: #e0f2fe; /* Warna Biru Muda */
color: #0369a1; /* Warna Biru Tua */
border: 1px solid #bae6fd;
}
.badge-soshum {
background-color: #fef3c7; /* Warna Kuning Muda */
color: #92400e; /* Warna Cokelat/Oren */
border: 1px solid #fde68a;
}
/* Base style untuk badge jika belum ada */
.badge {
display: inline-block;
padding: 0.35em 0.65em;
font-size: 0.75em;
font-weight: 700;
line-height: 1;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
border-radius: 50rem;
}

View File

@ -0,0 +1,60 @@
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap');
:root {
--asgard-green: #064e3b;
--asgard-lime: #10b981;
--asgard-yellow: #fbbf24;
}
html { scroll-behavior: smooth; }
body {
font-family: 'Plus Jakarta Sans', sans-serif;
/* Background Modern Gradasi Hijau - Kuning */
background:
radial-gradient(at 0% 0%, rgba(16, 185, 129, 0.15) 0px, transparent 50%),
radial-gradient(at 100% 100%, rgba(251, 191, 36, 0.1) 0px, transparent 50%),
linear-gradient(180deg, #ffffff 0%, #f0fdf4 100%);
color: #1e293b;
}
/* Bento Card Glassmorphism */
.bento-card {
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.5);
border-radius: 2.5rem;
transition: all 0.5s cubic-bezier(0.23, 1, 0.32, 1);
}
.bento-card:hover {
transform: translateY(-12px);
box-shadow: 0 30px 60px rgba(6, 78, 59, 0.12);
border-color: var(--asgard-lime);
}
/* Swiper Slider Styling */
.swiper-pagination-bullet-active {
background: var(--asgard-green) !important;
}
.text-gradient {
background: linear-gradient(135deg, var(--asgard-green) 0%, var(--asgard-lime) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
/* Footer Dark Smapa */
.footer-asgard {
background: #022c22;
color: #ecfdf5;
}
/* Interactive Floating Animation */
.animate-float {
animation: float 6s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-20px); }
}

28
public/css/student.css Normal file
View File

@ -0,0 +1,28 @@
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;700;800&display=swap');
:root {
--asgard-dark: #064e3b;
--asgard-lime: #10b981;
}
body { font-family: 'Plus Jakarta Sans', sans-serif; }
.nav-link {
@apply transition-all duration-300 flex items-center gap-4 p-4 rounded-2xl text-slate-400 text-sm tracking-tight;
}
.nav-link.active {
background: #f0fdf4;
color: var(--asgard-dark);
@apply shadow-sm border border-emerald-50;
}
.nav-link:hover:not(.active) {
@apply bg-slate-50 text-emerald-600 translate-x-2;
}
.sidebar { background: white; border-right: 1px solid #f1f5f9; }
input:focus, select:focus {
@apply shadow-lg shadow-emerald-50;
}

View File

@ -0,0 +1,144 @@
@extends('admin.layout')
@section('content')
<link rel="stylesheet" href="{{ asset('css/fitur-admin.css') }}">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<div class="dashboard-section">
<div class="mb-4 d-flex justify-content-between align-items-center">
<div>
<h4 class="fw-bold text-dark">Manajemen Data Alumni</h4>
<p class="text-muted small">Kelola data historis lulusan untuk kebutuhan dataset sistem prediksi.</p>
</div>
<a href="{{ route('admin.alumni.create') }}" class="btn text-white px-4 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">
<i class="fas fa-plus me-2"></i>Tambah Data Alumni
</a>
</div>
<div class="table-container-modern shadow-sm">
<div class="table-responsive">
<table class="table table-modern mb-0">
<thead>
<tr>
<th class="text-center" width="100">ID Alumni</th>
<th>Nama Alumni</th>
<th>Perguruan Tinggi</th>
<th class="text-center">Jurusan Diambil</th>
<th class="text-center" width="150">Opsi</th>
</tr>
</thead>
<tbody id="alumni-table-body">
<tr><td colspan="5" class="text-center py-5 text-muted">Memuat data alumni...</td></tr>
</tbody>
</table>
</div>
<div class="pagination-container">
<div class="text-muted small" id="pagination-info">
Menampilkan 0 sampai 0 dari 0 data
</div>
<div class="d-flex gap-1" id="pagination-controls">
</div>
</div>
</div>
</div>
<script>
async function fetchAlumni(page = 1) {
const token = localStorage.getItem('access_token');
try {
const response = await fetch(`/api/admin/alumni?page=${page}`, {
headers: {
'Authorization': 'Bearer ' + token,
'Accept': 'application/json'
}
});
const result = await response.json();
const tbody = document.getElementById('alumni-table-body');
tbody.innerHTML = '';
if(!result.data || result.data.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" class="text-center py-4">Tidak ada data alumni.</td></tr>';
return;
}
result.data.forEach((item) => {
tbody.innerHTML += `
<tr>
<td class="text-center fw-bold text-muted">${item.id_alumni}</td>
<td class="fw-bold text-dark">${item.nama_alumni}</td>
<td class="text-muted small">${item.perguruan_tinggi}</td>
<td class="text-center"><span class="badge bg-light text-dark border px-3 py-2" style="border-radius:8px">${item.jurusan_diambil}</span></td>
<td class="text-center">
<div class="opsi-wrapper">
<a href="/admin/alumni/${item.id}/edit" class="btn-action btn-edit" title="Edit">
<i class="fas fa-edit"></i>
</a>
<button onclick="deleteAlumni(${item.id})" class="btn-action btn-delete" title="Hapus">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>`;
});
document.getElementById('pagination-info').innerText =
`Menampilkan ${result.from || 0} sampai ${result.to || 0} dari ${result.total || 0} data`;
renderPagination(result);
} catch (error) {
tbody.innerHTML = '<tr><td colspan="5" class="text-center text-danger">Gagal memuat data.</td></tr>';
}
}
function renderPagination(data) {
const controls = document.getElementById('pagination-controls');
controls.innerHTML = '';
const prevBtn = document.createElement('button');
prevBtn.className = 'page-link-custom';
prevBtn.innerHTML = '&laquo; Previous';
prevBtn.disabled = data.current_page === 1;
prevBtn.onclick = () => fetchAlumni(data.current_page - 1);
controls.appendChild(prevBtn);
const pageBtn = document.createElement('button');
pageBtn.className = 'page-link-custom active';
pageBtn.innerText = data.current_page;
controls.appendChild(pageBtn);
const nextBtn = document.createElement('button');
nextBtn.className = 'page-link-custom';
nextBtn.innerHTML = 'Next &raquo;';
nextBtn.disabled = data.current_page === data.last_page;
nextBtn.onclick = () => fetchAlumni(data.current_page + 1);
controls.appendChild(nextBtn);
}
async function deleteAlumni(id) {
const res = await Swal.fire({
title: 'Hapus Data?',
text: "Data alumni akan dihapus secara permanen!",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#ef4444',
confirmButtonText: 'Ya, Hapus'
});
if (res.isConfirmed) {
const token = localStorage.getItem('access_token');
try {
const response = await fetch('/api/admin/alumni/' + id, {
method: 'DELETE',
headers: { 'Authorization': 'Bearer ' + token }
});
if(response.ok) {
Swal.fire('Terhapus!', 'Data berhasil dihapus.', 'success');
fetchAlumni();
}
} catch (error) { Swal.fire('Error!', 'Koneksi gagal.', 'error'); }
}
}
document.addEventListener('DOMContentLoaded', () => fetchAlumni(1));
</script>
@endsection

View File

@ -0,0 +1,72 @@
@extends('admin.layout')
@section('content')
<link rel="stylesheet" href="{{ asset('css/fitur-admin.css') }}">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<div class="dashboard-section d-flex justify-content-center">
<div style="width: 100%; max-width: 700px;">
<div class="mb-4 text-center">
<h4 class="fw-bold text-dark">Tambah Data Alumni</h4>
<p class="text-muted small">Input data historis untuk memperkuat akurasi algoritma Random Forest.</p>
</div>
<div class="card border-0 shadow-sm rounded-4">
<div class="card-body p-5">
<form id="formCreateAlumni">
<div class="row g-4">
<div class="col-md-12 form-group-custom">
<label>Nama Lengkap Alumni</label>
<input type="text" id="nama_alumni" class="form-control input-custom" placeholder="Contoh: Ahmad Fauzi" required>
</div>
<div class="col-md-12 form-group-custom">
<label>Perguruan Tinggi</label>
<input type="text" id="perguruan_tinggi" class="form-control input-custom" placeholder="Contoh: Universitas Indonesia" required>
</div>
<div class="col-md-12 form-group-custom">
<label>Jurusan Kuliah yang Diambil</label>
<input type="text" id="jurusan_diambil" class="form-control input-custom" placeholder="Contoh: Teknik Informatika" required>
</div>
</div>
<div class="mt-5 d-flex gap-2 justify-content-end">
<button type="submit" id="btnSave" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">Simpan Alumni</button>
<a href="{{ route('admin.alumni') }}" class="btn btn-light px-4" style="border-radius: 10px;">Batal</a>
</div>
</form>
</div>
</div>
</div>
</div>
<script>
document.getElementById('formCreateAlumni').addEventListener('submit', async (e) => {
e.preventDefault();
const btn = document.getElementById('btnSave');
btn.disabled = true;
const payload = {
nama_alumni: document.getElementById('nama_alumni').value,
perguruan_tinggi: document.getElementById('perguruan_tinggi').value,
jurusan_diambil: document.getElementById('jurusan_diambil').value
};
try {
const response = await fetch('/api/admin/alumni', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('access_token'),
'Accept': 'application/json'
},
body: JSON.stringify(payload)
});
if (response.ok) {
Swal.fire({ icon: 'success', title: 'Berhasil!', confirmButtonColor: '#2D6A4F' })
.then(() => { window.location.href = "{{ route('admin.alumni') }}"; });
}
} catch (error) { Swal.fire('Error!', 'Gagal menghubungi server.', 'error'); }
finally { btn.disabled = false; }
});
</script>
@endsection

View File

@ -0,0 +1,92 @@
@extends('admin.layout')
@section('content')
<link rel="stylesheet" href="{{ asset('css/fitur-admin.css') }}">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<div class="dashboard-section d-flex justify-content-center">
<div style="width: 100%; max-width: 700px;">
<div class="mb-4 text-center">
<h4 class="fw-bold text-dark">Edit Data Alumni</h4>
<p id="display_id_alumni" class="text-muted small fw-bold">Memuat data...</p>
</div>
<div class="card border-0 shadow-sm rounded-4">
<div class="card-body p-5">
<form id="formEditAlumni">
<input type="hidden" id="alumni_id_db" value="{{ $id }}">
<div class="row g-4">
<div class="col-md-12 form-group-custom">
<label>Nama Lengkap Alumni</label>
<input type="text" id="nama_alumni" class="form-control input-custom" required>
</div>
<div class="col-md-12 form-group-custom">
<label>Perguruan Tinggi</label>
<input type="text" id="perguruan_tinggi" class="form-control input-custom" required>
</div>
<div class="col-md-12 form-group-custom">
<label>Jurusan Kuliah</label>
<input type="text" id="jurusan_diambil" class="form-control input-custom" required>
</div>
</div>
<div class="mt-5 d-flex gap-2 justify-content-end">
<button type="submit" id="btnUpdate" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">Perbarui Data</button>
<a href="{{ route('admin.alumni') }}" class="btn btn-light px-4" style="border-radius: 10px;">Batal</a>
</div>
</form>
</div>
</div>
</div>
</div>
<script>
const dbId = document.getElementById('alumni_id_db').value;
async function loadAlumni() {
try {
const response = await fetch(`/api/admin/alumni/${dbId}`, {
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('access_token') }
});
const result = await response.json();
if (response.ok) {
document.getElementById('nama_alumni').value = result.data.nama_alumni;
document.getElementById('perguruan_tinggi').value = result.data.perguruan_tinggi;
document.getElementById('jurusan_diambil').value = result.data.jurusan_diambil;
document.getElementById('display_id_alumni').innerText = `ID Alumni: ${result.data.id_alumni}`;
}
} catch (error) { console.error(error); }
}
document.getElementById('formEditAlumni').addEventListener('submit', async (e) => {
e.preventDefault();
const btn = document.getElementById('btnUpdate');
btn.disabled = true;
const payload = {
nama_alumni: document.getElementById('nama_alumni').value,
perguruan_tinggi: document.getElementById('perguruan_tinggi').value,
jurusan_diambil: document.getElementById('jurusan_diambil').value
};
try {
const response = await fetch(`/api/admin/alumni/${dbId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('access_token'),
'Accept': 'application/json'
},
body: JSON.stringify(payload)
});
if (response.ok) {
Swal.fire({ icon: 'success', title: 'Berhasil Diperbarui!', confirmButtonColor: '#2D6A4F' })
.then(() => { window.location.href = "{{ route('admin.alumni') }}"; });
}
} catch (error) { Swal.fire('Error!', 'Gagal update.', 'error'); }
finally { btn.disabled = false; }
});
document.addEventListener('DOMContentLoaded', loadAlumni);
</script>
@endsection

View File

@ -0,0 +1,138 @@
@extends('admin.layout')
@section('content')
<div class="mb-4">
<h4 class="fw-bold text-dark">Overview Statistik Aplikasi</h4>
<p class="text-muted small">Selamat datang di panel kendali sistem rekomendasi jurusan SMAN 4 Jember.</p>
</div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-info">
<p>Total Siswa</p>
<h3 id="stat-total-siswa">0</h3>
</div>
<div class="stat-icon icon-siswa">
<i class="fas fa-user-graduate"></i>
</div>
</div>
<div class="stat-card">
<div class="stat-info">
<p>Rekomendasi Diproses</p>
<h3 id="stat-total-rekomendasi">0</h3>
</div>
<div class="stat-icon icon-rekom">
<i class="fas fa-check-circle"></i>
</div>
</div>
<div class="stat-card">
<div class="stat-info">
<p>Rata-rata Akurasi</p>
<h3 id="stat-rata-akurasi">0%</h3>
</div>
<div class="stat-icon icon-akurasi">
<i class="fas fa-chart-line"></i>
</div>
</div>
</div>
<div class="dashboard-section mt-4">
<div class="chart-container">
<h5 class="fw-bold mb-4 d-flex align-items-center gap-2">
<div style="width: 4px; height: 20px; background: var(--primary-green); border-radius: 10px;"></div>
Distribusi Rekomendasi Jurusan SMAN 4 Jember
</h5>
<div id="chart-wrapper" style="height: 350px; display: flex; align-items: center; justify-content: center; background: #f8fafc; border-radius: 15px; border: 2px dashed #e2e8f0; padding: 20px;">
<canvas id="distribusiChart" class="d-none w-100 h-100"></canvas>
<div id="chart-placeholder" class="text-center">
<i class="fas fa-chart-pie fa-3x text-light mb-3"></i>
<p class="text-muted">Memuat grafik distribusi...</p>
</div>
</div>
</div>
</div>
@push('scripts')
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
document.addEventListener('DOMContentLoaded', async function() {
const token = localStorage.getItem('access_token');
if (!token) {
window.location.href = '/login';
return;
}
try {
const response = await fetch('/api/admin/dashboard-stats', {
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/json'
}
});
const result = await response.json();
if (result.success) {
const data = result.data;
document.getElementById('stat-total-siswa').innerText = data.total_siswa;
document.getElementById('stat-total-rekomendasi').innerText = data.rekomendasi_diproses;
document.getElementById('stat-rata-akurasi').innerText = data.rata_rata_akurasi + '%';
const distribusi = data.distribusi_jurusan;
const placeholder = document.getElementById('chart-placeholder');
const canvas = document.getElementById('distribusiChart');
const wrapper = document.getElementById('chart-wrapper');
if (distribusi && distribusi.length > 0) {
placeholder.classList.add('d-none');
canvas.classList.remove('d-none');
wrapper.style.border = 'none';
wrapper.style.background = 'transparent';
const labels = distribusi.map(item => item.jurusan);
const counts = distribusi.map(item => item.total);
const ctx = canvas.getContext('2d');
new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'Jumlah Rekomendasi',
data: counts,
backgroundColor: 'rgba(45, 122, 71, 0.8)',
borderColor: 'rgba(45, 122, 71, 1)',
borderWidth: 1,
borderRadius: 5
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
ticks: { stepSize: 1 }
}
},
plugins: {
legend: { display: false }
}
}
});
} else {
placeholder.querySelector('p').innerText = 'Belum ada data riwayat rekomendasi.';
}
}
} catch (error) {
console.error('Gagal memuat data dashboard:', error);
document.getElementById('chart-placeholder').querySelector('p').innerText = 'Gagal memuat data.';
}
});
</script>
@endpush
@endsection

View File

@ -0,0 +1,145 @@
@extends('admin.layout')
@section('content')
<link rel="stylesheet" href="{{ asset('css/fitur-admin.css') }}">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<div class="dashboard-section">
<div class="mb-4 d-flex justify-content-between align-items-center">
<div>
<h4 class="fw-bold text-dark">Data Guru BK</h4>
<p class="text-muted small">Kelola daftar administrator dan guru pembimbing SMAN 4 Jember.</p>
</div>
<a href="{{ route('admin.guru.create') }}" class="btn text-white px-4 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">
<i class="fas fa-plus me-2"></i>Tambah Guru BK
</a>
</div>
<div class="table-container-modern shadow-sm">
<div class="table-responsive">
<table class="table table-modern mb-0">
<thead>
<tr>
<th class="text-center" width="100">ID Guru</th>
<th>Nama Lengkap & Gelar</th>
<th class="text-center" width="150">Opsi</th>
</tr>
</thead>
<tbody id="guru-table-body">
<tr><td colspan="3" class="text-center py-5 text-muted">Memuat data...</td></tr>
</tbody>
</table>
</div>
<div class="pagination-container">
<div class="text-muted small" id="pagination-info">
Menampilkan 0 sampai 0 dari 0 data
</div>
<div class="d-flex gap-1" id="pagination-controls">
</div>
</div>
</div>
</div>
<script>
async function fetchGuru(page = 1) {
const token = localStorage.getItem('access_token');
try {
const response = await fetch(`/api/admin/guru?page=${page}`, {
headers: {
'Authorization': 'Bearer ' + token,
'Accept': 'application/json'
}
});
const result = await response.json();
const tbody = document.getElementById('guru-table-body');
tbody.innerHTML = '';
if(!result.data || result.data.length === 0) {
tbody.innerHTML = '<tr><td colspan="3" class="text-center py-4">Tidak ada data guru.</td></tr>';
return;
}
result.data.forEach((item) => {
tbody.innerHTML += `
<tr>
<td class="text-center fw-bold text-muted">${item.id_guru}</td>
<td>
<div class="d-flex align-items-center gap-3">
<span class="badge badge-bk rounded-circle d-flex align-items-center justify-content-center" style="width: 30px; height: 30px;">BK</span>
<b class="text-dark">${item.nama_guru}</b>
</div>
</td>
<td class="text-center">
<div class="opsi-wrapper">
<a href="/admin/guru/${item.id}/edit" class="btn-action btn-edit" title="Edit">
<i class="fas fa-edit"></i>
</a>
<button onclick="deleteGuru(${item.id})" class="btn-action btn-delete" title="Hapus">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>`;
});
document.getElementById('pagination-info').innerText =
`Menampilkan ${result.from || 0} sampai ${result.to || 0} dari ${result.total || 0} data`;
renderPagination(result);
} catch (error) {
tbody.innerHTML = '<tr><td colspan="3" class="text-center text-danger">Gagal memuat data.</td></tr>';
}
}
function renderPagination(data) {
const controls = document.getElementById('pagination-controls');
controls.innerHTML = '';
const prevBtn = document.createElement('button');
prevBtn.className = 'page-link-custom';
prevBtn.innerHTML = '&laquo; Previous';
prevBtn.disabled = data.current_page === 1;
prevBtn.onclick = () => fetchGuru(data.current_page - 1);
controls.appendChild(prevBtn);
const pageBtn = document.createElement('button');
pageBtn.className = 'page-link-custom active';
pageBtn.innerText = data.current_page || 1;
controls.appendChild(pageBtn);
const nextBtn = document.createElement('button');
nextBtn.className = 'page-link-custom';
nextBtn.innerHTML = 'Next &raquo;';
nextBtn.disabled = data.current_page === data.last_page;
nextBtn.onclick = () => fetchGuru(data.current_page + 1);
controls.appendChild(nextBtn);
}
async function deleteGuru(id) {
const res = await Swal.fire({
title: 'Hapus Guru?',
text: "Data guru akan dihapus permanen!",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#ef4444',
confirmButtonText: 'Ya, Hapus'
});
if (res.isConfirmed) {
const token = localStorage.getItem('access_token');
try {
const response = await fetch('/api/admin/guru/' + id, {
method: 'DELETE',
headers: { 'Authorization': 'Bearer ' + token }
});
if(response.ok) {
Swal.fire('Terhapus!', 'Data guru berhasil dihapus.', 'success');
fetchGuru();
}
} catch (error) { Swal.fire('Error!', 'Koneksi gagal.', 'error'); }
}
}
document.addEventListener('DOMContentLoaded', () => fetchGuru(1));
</script>
@endsection

View File

@ -0,0 +1,62 @@
@extends('admin.layout')
@section('content')
<link rel="stylesheet" href="{{ asset('css/fitur-admin.css') }}">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<div class="dashboard-section d-flex justify-content-center">
<div style="width: 100%; max-width: 700px;">
<div class="mb-4 text-center">
<h4 class="fw-bold text-dark">Tambah Guru BK</h4>
<p class="text-muted small">Daftarkan akun guru pembimbing baru untuk sistem rekomendasi.</p>
</div>
<div class="card border-0 shadow-sm rounded-4">
<div class="card-body p-5">
<form id="formCreateGuru">
<div class="row g-4">
<div class="col-md-12 form-group-custom">
<label class="fw-bold small text-muted">Nama Lengkap & Gelar</label>
<input type="text" id="nama_guru" class="form-control input-custom" placeholder="Contoh: Budi Santoso, S.Pd" required>
</div>
</div>
<div class="mt-5 d-flex gap-2 justify-content-end">
<button type="submit" id="btnSave" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">Daftarkan Guru</button>
<a href="{{ route('admin.guru') }}" class="btn btn-light px-4" style="border-radius: 10px;">Batal</a>
</div>
</form>
</div>
</div>
</div>
</div>
<script>
document.getElementById('formCreateGuru').addEventListener('submit', async (e) => {
e.preventDefault();
const btn = document.getElementById('btnSave');
btn.disabled = true;
const payload = {
nama_guru: document.getElementById('nama_guru').value
};
try {
const response = await fetch('/api/admin/guru', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('access_token'),
'Accept': 'application/json'
},
body: JSON.stringify(payload)
});
if (response.ok) {
Swal.fire({ icon: 'success', title: 'Berhasil!', confirmButtonColor: '#2D6A4F' })
.then(() => { window.location.href = "{{ route('admin.guru') }}"; });
}
} catch (error) { Swal.fire('Error!', 'Gagal menghubungi server.', 'error'); }
finally { btn.disabled = false; }
});
</script>
@endsection

View File

@ -0,0 +1,80 @@
@extends('admin.layout')
@section('content')
<link rel="stylesheet" href="{{ asset('css/fitur-admin.css') }}">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<div class="dashboard-section d-flex justify-content-center">
<div style="width: 100%; max-width: 700px;">
<div class="mb-4 text-center">
<h4 class="fw-bold text-dark">Edit Data Guru</h4>
<p id="display_id_guru" class="text-muted small fw-bold">Memuat profil...</p>
</div>
<div class="card border-0 shadow-sm rounded-4">
<div class="card-body p-5">
<form id="formEditGuru">
<input type="hidden" id="guru_id_db" value="{{ $id }}">
<div class="row g-4">
<div class="col-md-12 form-group-custom">
<label class="fw-bold small text-muted">Nama Lengkap & Gelar</label>
<input type="text" id="nama_guru" class="form-control input-custom" required>
</div>
</div>
<div class="mt-5 d-flex gap-2 justify-content-end">
<button type="submit" id="btnUpdate" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">Update Profil</button>
<a href="{{ route('admin.guru') }}" class="btn btn-light px-4" style="border-radius: 10px;">Batal</a>
</div>
</form>
</div>
</div>
</div>
</div>
<script>
const dbId = document.getElementById('guru_id_db').value;
async function loadGuru() {
try {
const response = await fetch(`/api/admin/guru/${dbId}`, {
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('access_token') }
});
const result = await response.json();
if (response.ok) {
document.getElementById('nama_guru').value = result.data.nama_guru;
document.getElementById('display_id_guru').innerText = `Ubah profil administrator guru BK (${result.data.id_guru}).`;
}
} catch (error) { console.error(error); }
}
document.getElementById('formEditGuru').addEventListener('submit', async (e) => {
e.preventDefault();
const btn = document.getElementById('btnUpdate');
btn.disabled = true;
const payload = {
nama_guru: document.getElementById('nama_guru').value
};
try {
const response = await fetch(`/api/admin/guru/${dbId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('access_token'),
'Accept': 'application/json'
},
body: JSON.stringify(payload)
});
if (response.ok) {
Swal.fire({ icon: 'success', title: 'Berhasil Diperbarui!', confirmButtonColor: '#2D6A4F' })
.then(() => { window.location.href = "{{ route('admin.guru') }}"; });
}
} catch (error) { Swal.fire('Error!', 'Gagal koneksi.', 'error'); }
finally { btn.disabled = false; }
});
document.addEventListener('DOMContentLoaded', loadGuru);
</script>
@endsection

View File

@ -0,0 +1,153 @@
@extends('admin.layout')
@section('content')
<link rel="stylesheet" href="{{ asset('css/fitur-admin.css') }}">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<div class="dashboard-section">
<div class="mb-4 d-flex justify-content-between align-items-center">
<div>
<h4 class="fw-bold text-dark">Data Master Jurusan</h4>
<p class="text-muted small">Kelola daftar jurusan kuliah yang tersedia dalam sistem prediksi.</p>
</div>
<a href="{{ route('admin.jurusan.create') }}" class="btn text-white px-4 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">
<i class="fas fa-plus me-2"></i>Tambah Jurusan
</a>
</div>
<div class="table-container-modern shadow-sm">
<div class="table-responsive">
<table class="table table-modern mb-0">
<thead>
<tr>
<th class="text-center" width="60">No</th>
<th>Nama Jurusan</th>
<th class="text-center">Rumpun</th>
<th>Prospek Karir</th>
<th class="text-center" width="150">Opsi</th>
</tr>
</thead>
<tbody id="jurusan-table-body">
<tr><td colspan="5" class="text-center py-5 text-muted">Memuat data...</td></tr>
</tbody>
</table>
</div>
<div class="pagination-container">
<div class="text-muted small" id="pagination-info">
Menampilkan 0 sampai 0 dari 0 data
</div>
<div class="d-flex gap-1" id="pagination-controls">
</div>
</div>
</div>
</div>
<script>
async function fetchJurusan(page = 1) {
const token = localStorage.getItem('access_token');
try {
const response = await fetch(`/api/admin/jurusan?page=${page}`, {
headers: {
'Authorization': 'Bearer ' + token,
'Accept': 'application/json'
}
});
const result = await response.json();
const tbody = document.getElementById('jurusan-table-body');
tbody.innerHTML = '';
if(!result.data || result.data.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" class="text-center py-4">Tidak ada data jurusan.</td></tr>';
return;
}
result.data.forEach((item, index) => {
const rumpunClass = item.rumpun.toLowerCase() === 'saintek' ? 'badge-saintek' : 'badge-soshum';
const row = `
<tr>
<td class="text-center text-muted">${(result.current_page - 1) * result.per_page + (index + 1)}</td>
<td class="fw-bold text-dark text-uppercase">${item.nama_jurusan}</td>
<td class="text-center">
<span class="badge rounded-pill px-3 py-2 ${rumpunClass}">
${item.rumpun}
</span>
</td>
<td class="small text-muted">${item.prospek_karir || '-'}</td>
<td class="text-center">
<div class="opsi-wrapper">
<a href="/admin/jurusan/${item.id}/edit" class="btn-action btn-edit" title="Edit">
<i class="fas fa-edit"></i>
</a>
<button onclick="deleteJurusan(${item.id})" class="btn-action btn-delete" title="Hapus">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>`;
tbody.innerHTML += row;
});
document.getElementById('pagination-info').innerText =
`Menampilkan ${result.from || 0} sampai ${result.to || 0} dari ${result.total || 0} data`;
renderPagination(result);
} catch (error) {
tbody.innerHTML = '<tr><td colspan="5" class="text-center text-danger">Gagal memuat data.</td></tr>';
}
}
function renderPagination(data) {
const controls = document.getElementById('pagination-controls');
controls.innerHTML = '';
const prevBtn = document.createElement('button');
prevBtn.className = 'page-link-custom';
prevBtn.innerHTML = '&laquo; Previous';
prevBtn.disabled = data.current_page === 1;
prevBtn.onclick = () => fetchJurusan(data.current_page - 1);
controls.appendChild(prevBtn);
const pageBtn = document.createElement('button');
pageBtn.className = 'page-link-custom active';
pageBtn.innerText = data.current_page;
controls.appendChild(pageBtn);
const nextBtn = document.createElement('button');
nextBtn.className = 'page-link-custom';
nextBtn.innerHTML = 'Next &raquo;';
nextBtn.disabled = data.current_page === data.last_page;
nextBtn.onclick = () => fetchJurusan(data.current_page + 1);
controls.appendChild(nextBtn);
}
async function deleteJurusan(id) {
const res = await Swal.fire({
title: 'Hapus data?',
text: "Data yang dihapus tidak dapat dikembalikan!",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#ef4444',
confirmButtonText: 'Ya, Hapus'
});
if (res.isConfirmed) {
const token = localStorage.getItem('access_token');
try {
const response = await fetch('/api/admin/jurusan/' + id, {
method: 'DELETE',
headers: { 'Authorization': 'Bearer ' + token }
});
if(response.ok) {
Swal.fire('Terhapus!', 'Data berhasil dihapus.', 'success');
fetchJurusan();
}
} catch (error) { Swal.fire('Error!', 'Koneksi gagal.', 'error'); }
}
}
document.addEventListener('DOMContentLoaded', () => fetchJurusan(1));
</script>
@endsection

View File

@ -0,0 +1,76 @@
@extends('admin.layout')
@section('content')
<link rel="stylesheet" href="{{ asset('css/fitur-admin.css') }}">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<div class="dashboard-section d-flex justify-content-center">
<div style="width: 100%; max-width: 700px;">
<div class="mb-4 text-center">
<h4 class="fw-bold text-dark">Tambah Jurusan Baru</h4>
<p class="text-muted small">Tambahkan pilihan program studi baru ke dalam sistem prediksi.</p>
</div>
<div class="card border-0 shadow-sm rounded-4">
<div class="card-body p-5">
<form id="formCreateJurusan">
<div class="row g-4">
<div class="col-md-12 form-group-custom">
<label class="fw-bold small text-muted">Nama Jurusan</label>
<input type="text" id="nama_jurusan" class="form-control input-custom" placeholder="Contoh: Teknik Elektro" required>
</div>
<div class="col-md-12 form-group-custom">
<label class="fw-bold small text-muted">Rumpun</label>
<select id="rumpun" class="form-select input-custom" required>
<option value="" disabled selected>Pilih Rumpun...</option>
<option value="Saintek">Saintek</option>
<option value="Soshum">Soshum</option>
</select>
</div>
<div class="col-md-12 form-group-custom">
<label class="fw-bold small text-muted">Prospek Karir</label>
<textarea id="prospek_karir" class="form-control input-custom" rows="3" placeholder="Contoh: Engineer, Analyst, Developer"></textarea>
</div>
</div>
<div class="mt-5 d-flex gap-2 justify-content-end">
<button type="submit" id="btnSave" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">Simpan Data</button>
<a href="{{ route('admin.jurusan') }}" class="btn btn-light px-4" style="border-radius: 10px;">Batal</a>
</div>
</form>
</div>
</div>
</div>
</div>
<script>
document.getElementById('formCreateJurusan').addEventListener('submit', async (e) => {
e.preventDefault();
const btn = document.getElementById('btnSave');
btn.disabled = true;
const payload = {
nama_jurusan: document.getElementById('nama_jurusan').value,
rumpun: document.getElementById('rumpun').value,
prospek_karir: document.getElementById('prospek_karir').value
};
try {
const response = await fetch('/api/admin/jurusan', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('access_token'),
'Accept': 'application/json'
},
body: JSON.stringify(payload)
});
if (response.ok) {
Swal.fire({ icon: 'success', title: 'Berhasil!', confirmButtonColor: '#2D6A4F' })
.then(() => { window.location.href = "{{ route('admin.jurusan') }}"; });
}
} catch (error) { Swal.fire('Error!', 'Gagal menghubungi server.', 'error'); }
finally { btn.disabled = false; }
});
</script>
@endsection

View File

@ -0,0 +1,94 @@
@extends('admin.layout')
@section('content')
<link rel="stylesheet" href="{{ asset('css/fitur-admin.css') }}">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<div class="dashboard-section d-flex justify-content-center">
<div style="width: 100%; max-width: 700px;">
<div class="mb-4 text-center">
<h4 class="fw-bold text-dark">Edit Data Jurusan</h4>
<p class="text-muted small">Ubah informasi nama dan rumpun program studi.</p>
</div>
<div class="card border-0 shadow-sm rounded-4">
<div class="card-body p-5">
<form id="formEditJurusan">
<input type="hidden" id="jurusan_id" value="{{ $id }}">
<div class="row g-4">
<div class="col-md-12 form-group-custom">
<label class="fw-bold small text-muted">Nama Jurusan</label>
<input type="text" id="nama_jurusan" class="form-control input-custom" required>
</div>
<div class="col-md-12 form-group-custom">
<label class="fw-bold small text-muted">Rumpun</label>
<select id="rumpun" class="form-select input-custom" required>
<option value="Saintek">Saintek</option>
<option value="Soshum">Soshum</option>
</select>
</div>
<div class="col-md-12 form-group-custom">
<label class="fw-bold small text-muted">Prospek Karir</label>
<textarea id="prospek_karir" class="form-control input-custom" rows="3"></textarea>
</div>
</div>
<div class="mt-5 d-flex gap-2 justify-content-end">
<button type="submit" id="btnUpdate" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">Perbarui Data</button>
<a href="{{ route('admin.jurusan') }}" class="btn btn-light px-4" style="border-radius: 10px;">Batal</a>
</div>
</form>
</div>
</div>
</div>
</div>
<script>
const jurusanId = document.getElementById('jurusan_id').value;
async function loadData() {
try {
const response = await fetch(`/api/admin/jurusan/${jurusanId}`, {
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('access_token') }
});
const result = await response.json();
if (response.ok) {
document.getElementById('nama_jurusan').value = result.data.nama_jurusan;
document.getElementById('rumpun').value = result.data.rumpun;
document.getElementById('prospek_karir').value = result.data.prospek_karir || '';
}
} catch (error) { console.error(error); }
}
document.getElementById('formEditJurusan').addEventListener('submit', async (e) => {
e.preventDefault();
const btn = document.getElementById('btnUpdate');
btn.disabled = true;
const payload = {
nama_jurusan: document.getElementById('nama_jurusan').value,
rumpun: document.getElementById('rumpun').value,
prospek_karir: document.getElementById('prospek_karir').value
};
try {
const response = await fetch(`/api/admin/jurusan/${jurusanId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('access_token'),
'Accept': 'application/json'
},
body: JSON.stringify(payload)
});
if (response.ok) {
Swal.fire({ icon: 'success', title: 'Berhasil Diperbarui!', confirmButtonColor: '#2D6A4F' })
.then(() => { window.location.href = "{{ route('admin.jurusan') }}"; });
}
} catch (error) { Swal.fire('Error!', 'Gagal koneksi.', 'error'); }
finally { btn.disabled = false; }
});
document.addEventListener('DOMContentLoaded', loadData);
</script>
@endsection

View File

@ -0,0 +1,40 @@
@extends('admin.layout')
@section('content')
<div class="mb-4">
<h4 class="fw-bold">Pelatihan Model Random Forest</h4>
<p class="text-muted small">Kelola dataset alumni untuk meningkatkan akurasi sistem.</p>
</div>
<div class="row g-4">
<div class="col-md-4">
<div class="card border-0 shadow-sm rounded-4 p-4 text-center">
<div class="mb-3 text-success"><i class="fas fa-database fa-3x"></i></div>
<h6 class="fw-bold">Dataset Alumni</h6>
<h2 class="fw-black my-2">120</h2>
<p class="text-muted small">Data siap latih</p>
<button class="btn btn-success w-100 rounded-pill py-3 mt-3 fw-bold" onclick="runAI()">
JALANKAN TRAINING
</button>
</div>
</div>
<div class="col-md-8">
<div class="card border-0 shadow-sm rounded-4 p-4 bg-dark text-white font-monospace" style="min-height: 280px;">
<div id="ai-log">
<p class="text-success mb-1">> System Ready. Waiting for command...</p>
</div>
</div>
</div>
</div>
<script>
function runAI() {
const log = document.getElementById('ai-log');
log.innerHTML += '<p class="mb-1 text-info">> Connecting to Flask API Python...</p>';
setTimeout(() => {
log.innerHTML += '<p class="mb-1">> Fetching dataset from MySQL...</p>';
log.innerHTML += '<p class="mb-1 text-warning">> Random Forest: Training 100 decision trees...</p>';
log.innerHTML += '<p class="mb-1 text-success">> Accuracy Result: 86.4%</p>';
}, 1500);
}
</script>
@endsection

View File

@ -0,0 +1,95 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>Admin - Sistem Rekomendasi Jurusan SMAN 4 Jember</title>
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600,700&display=swap" rel="stylesheet" />
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet"/>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="{{ asset('css/admin.css') }}" rel="stylesheet">
@stack('styles')
</head>
<body>
<div class="admin-wrapper">
<aside class="sidebar-modern">
<div class="sidebar-header">
<img src="{{ asset('images/logo.png') }}" alt="Logo SMAN 4 Jember" onerror="this.src='https://ui-avatars.com/api/?name=S4&background=2D7A47&color=fff'">
<div class="sidebar-brand-text fw-bold text-white">SMAN 4 JEMBER</div>
<div class="sidebar-brand-sub small" style="opacity: 0.8;">SISTEM REKOMENDASI</div>
</div>
<nav class="sidebar-menu">
<div class="menu-label">Utama</div>
<a href="{{ route('admin.dashboard') }}" class="nav-item {{ request()->routeIs('admin.dashboard') ? 'active' : '' }}">
<i class="fas fa-th-large"></i> Dashboard
</a>
<div class="menu-label">Manajemen Data</div>
<a href="{{ route('admin.siswa') }}" class="nav-item {{ request()->routeIs('admin.siswa*') ? 'active' : '' }}">
<i class="fas fa-user-graduate"></i> Data Siswa
</a>
<a href="{{ route('admin.jurusan') }}" class="nav-item {{ request()->routeIs('admin.jurusan*') ? 'active' : '' }}">
<i class="fas fa-university"></i> Data Jurusan
</a>
<a href="{{ route('admin.prospek-kerja') }}" class="nav-item {{ request()->routeIs('admin.prospek-kerja*') ? 'active' : '' }}">
<i class="fas fa-briefcase"></i> Data Prospek Kerja
</a>
<a href="{{ route('admin.guru') }}" class="nav-item {{ request()->routeIs('admin.guru*') ? 'active' : '' }}">
<i class="fas fa-user-tie"></i> Guru BK
</a>
<a href="{{ route('admin.alumni') }}" class="nav-item {{ request()->routeIs('admin.alumni*') ? 'active' : '' }}">
<i class="fas fa-graduation-cap"></i> Data Alumni
</a>
<div class="menu-label">Analisis & Riwayat</div>
<a href="{{ route('admin.riwayat') }}" class="nav-item {{ request()->routeIs('admin.riwayat*') ? 'active' : '' }}">
<i class="fas fa-history"></i> Riwayat Prediksi
</a>
<a href="{{ route('admin.latih-model') }}" class="nav-item {{ request()->routeIs('admin.latih-model') ? 'active' : '' }}">
<i class="fas fa-brain"></i> Latih Model (RF)
</a>
<div class="mt-4 pt-4 border-top border-white border-opacity-10">
<a href="{{ route('landingpage') }}" class="nav-item text-white opacity-75">
<i class="fas fa-sign-out-alt"></i> Keluar Ke Landing
</a>
</div>
</nav>
</aside>
<div class="main-content">
<header class="topbar">
<div class="search-bar">
<i class="fas fa-search text-muted"></i>
<input type="text" placeholder="Cari data...">
</div>
<div class="user-profile d-flex align-items-center gap-3">
<div class="text-end d-none d-sm-block">
<div class="fw-bold small">Administrator</div>
<div class="text-muted" style="font-size: 11px;">GURU BK</div>
</div>
<img src="https://ui-avatars.com/api/?name=Admin+BK&background=2D7A47&color=fff&rounded=true" width="40" height="40">
</div>
</header>
<main class="p-4">
@yield('content')
</main>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
@stack('scripts')
</body>
</html>

View File

@ -0,0 +1,111 @@
@extends('admin.layout')
@section('content')
<link rel="stylesheet" href="{{ asset('css/fitur-admin.css') }}">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<div class="dashboard-section">
<div class="mb-4 d-flex justify-content-between align-items-center">
<div>
<h4 class="fw-bold text-dark">Data Master Prospek Kerja</h4>
<p class="text-muted small">Kelola daftar karir dan estimasi gaji untuk setiap jurusan.</p>
</div>
<a href="{{ route('admin.prospek-kerja.create') }}" class="btn text-white px-4 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">
<i class="fas fa-plus me-2"></i>Tambah Prospek
</a>
</div>
<div class="table-container-modern shadow-sm">
<div class="table-responsive">
<table class="table table-modern mb-0">
<thead>
<tr>
<th class="text-center" width="60">No</th>
<th>Nama Profesi</th>
<th>Asal Jurusan</th>
<th>Estimasi Gaji</th>
<th class="text-center" width="150">Opsi</th>
</tr>
</thead>
<tbody id="prospek-table-body">
<tr><td colspan="5" class="text-center py-5 text-muted">Memuat data...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<script>
async function fetchProspek() {
const token = localStorage.getItem('access_token');
try {
const response = await fetch(`/api/admin/prospek-kerja`, {
headers: {
'Authorization': 'Bearer ' + token,
'Accept': 'application/json'
}
});
const result = await response.json();
const tbody = document.getElementById('prospek-table-body');
tbody.innerHTML = '';
if(!result.data || result.data.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" class="text-center py-4">Tidak ada data prospek kerja.</td></tr>';
return;
}
result.data.forEach((item, index) => {
const namaJurusan = item.jurusan ? item.jurusan.nama_jurusan : '<span class="text-danger">Jurusan Dihapus</span>';
const row = `
<tr>
<td class="text-center text-muted">${index + 1}</td>
<td class="fw-bold text-dark">${item.nama_profesi}</td>
<td><span class="badge bg-light text-dark border px-3 py-2">${namaJurusan}</span></td>
<td class="small text-success fw-bold">${item.range_gaji || 'Belum diatur'}</td>
<td class="text-center">
<div class="opsi-wrapper">
<a href="/admin/prospek-kerja/${item.id}/edit" class="btn-action btn-edit" title="Edit">
<i class="fas fa-edit"></i>
</a>
<button onclick="deleteProspek(${item.id})" class="btn-action btn-delete" title="Hapus">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>`;
tbody.innerHTML += row;
});
} catch (error) {
document.getElementById('prospek-table-body').innerHTML = '<tr><td colspan="5" class="text-center text-danger">Gagal memuat data.</td></tr>';
}
}
async function deleteProspek(id) {
const res = await Swal.fire({
title: 'Hapus data?',
text: "Data yang dihapus tidak dapat dikembalikan!",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#ef4444',
confirmButtonText: 'Ya, Hapus'
});
if (res.isConfirmed) {
const token = localStorage.getItem('access_token');
try {
const response = await fetch('/api/admin/prospek-kerja/' + id, {
method: 'DELETE',
headers: { 'Authorization': 'Bearer ' + token }
});
if(response.ok) {
Swal.fire('Terhapus!', 'Data berhasil dihapus.', 'success');
fetchProspek();
}
} catch (error) { Swal.fire('Error!', 'Koneksi gagal.', 'error'); }
}
}
document.addEventListener('DOMContentLoaded', fetchProspek);
</script>
@endsection

View File

@ -0,0 +1,100 @@
@extends('admin.layout')
@section('content')
<link rel="stylesheet" href="{{ asset('css/fitur-admin.css') }}">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<div class="dashboard-section d-flex justify-content-center">
<div style="width: 100%; max-width: 700px;">
<div class="mb-4 text-center">
<h4 class="fw-bold text-dark">Tambah Prospek Kerja Baru</h4>
<p class="text-muted small">Tambahkan karir untuk direkomendasikan pada jurusan tertentu.</p>
</div>
<div class="card border-0 shadow-sm rounded-4">
<div class="card-body p-5">
<form id="formCreateProspek">
<div class="row g-4">
<div class="col-md-12 form-group-custom">
<label class="fw-bold small text-muted">Kaitkan dengan Jurusan</label>
<select id="jurusan_id" class="form-select input-custom" required>
<option value="" disabled selected>Memuat daftar jurusan...</option>
</select>
</div>
<div class="col-md-12 form-group-custom">
<label class="fw-bold small text-muted">Nama Profesi</label>
<input type="text" id="nama_profesi" class="form-control input-custom" placeholder="Contoh: Arsitek Profesional" required>
</div>
<div class="col-md-12 form-group-custom">
<label class="fw-bold small text-muted">Range Gaji (Opsional)</label>
<input type="text" id="range_gaji" class="form-control input-custom" placeholder="Contoh: Rp 6.000.000 - Rp 15.000.000">
</div>
<div class="col-md-12 form-group-custom">
<label class="fw-bold small text-muted">Deskripsi Pekerjaan (Opsional)</label>
<textarea id="deskripsi_pekerjaan" class="form-control input-custom" rows="3" placeholder="Jelaskan peran pekerjaan ini secara singkat..."></textarea>
</div>
</div>
<div class="mt-5 d-flex gap-2 justify-content-end">
<button type="submit" id="btnSave" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">Simpan Data</button>
<a href="{{ route('admin.prospek-kerja') }}" class="btn btn-light px-4" style="border-radius: 10px;">Batal</a>
</div>
</form>
</div>
</div>
</div>
</div>
<script>
async function loadJurusan() {
try {
const response = await fetch('/api/admin/jurusan', {
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('access_token') }
});
const result = await response.json();
const select = document.getElementById('jurusan_id');
select.innerHTML = '<option value="" disabled selected>Pilih Jurusan Terkait...</option>';
result.data.forEach(item => {
select.innerHTML += `<option value="${item.id}">${item.nama_jurusan}</option>`;
});
} catch (error) {
document.getElementById('jurusan_id').innerHTML = '<option disabled>Gagal memuat jurusan</option>';
}
}
document.getElementById('formCreateProspek').addEventListener('submit', async (e) => {
e.preventDefault();
const btn = document.getElementById('btnSave');
btn.disabled = true;
const payload = {
jurusan_id: document.getElementById('jurusan_id').value,
nama_profesi: document.getElementById('nama_profesi').value,
range_gaji: document.getElementById('range_gaji').value,
deskripsi_pekerjaan: document.getElementById('deskripsi_pekerjaan').value
};
try {
const response = await fetch('/api/admin/prospek-kerja', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('access_token'),
'Accept': 'application/json'
},
body: JSON.stringify(payload)
});
if (response.ok) {
Swal.fire({ icon: 'success', title: 'Berhasil!', confirmButtonColor: '#2D6A4F' })
.then(() => { window.location.href = "{{ route('admin.prospek-kerja') }}"; });
} else {
Swal.fire('Error!', 'Periksa kembali isian Anda.', 'error');
}
} catch (error) { Swal.fire('Error!', 'Gagal menghubungi server.', 'error'); }
finally { btn.disabled = false; }
});
document.addEventListener('DOMContentLoaded', loadJurusan);
</script>
@endsection

View File

@ -0,0 +1,113 @@
@extends('admin.layout')
@section('content')
<link rel="stylesheet" href="{{ asset('css/fitur-admin.css') }}">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<div class="dashboard-section d-flex justify-content-center">
<div style="width: 100%; max-width: 700px;">
<div class="mb-4 text-center">
<h4 class="fw-bold text-dark">Edit Data Prospek Kerja</h4>
<p class="text-muted small">Ubah informasi profesi atau estimasi gaji.</p>
</div>
<div class="card border-0 shadow-sm rounded-4">
<div class="card-body p-5">
<form id="formEditProspek">
<input type="hidden" id="prospek_id" value="{{ $id }}">
<div class="row g-4">
<div class="col-md-12 form-group-custom">
<label class="fw-bold small text-muted">Kaitkan dengan Jurusan</label>
<select id="jurusan_id" class="form-select input-custom" required>
<option value="" disabled>Memuat daftar jurusan...</option>
</select>
</div>
<div class="col-md-12 form-group-custom">
<label class="fw-bold small text-muted">Nama Profesi</label>
<input type="text" id="nama_profesi" class="form-control input-custom" required>
</div>
<div class="col-md-12 form-group-custom">
<label class="fw-bold small text-muted">Range Gaji</label>
<input type="text" id="range_gaji" class="form-control input-custom">
</div>
<div class="col-md-12 form-group-custom">
<label class="fw-bold small text-muted">Deskripsi Pekerjaan</label>
<textarea id="deskripsi_pekerjaan" class="form-control input-custom" rows="3"></textarea>
</div>
</div>
<div class="mt-5 d-flex gap-2 justify-content-end">
<button type="submit" id="btnUpdate" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">Perbarui Data</button>
<a href="{{ route('admin.prospek-kerja') }}" class="btn btn-light px-4" style="border-radius: 10px;">Batal</a>
</div>
</form>
</div>
</div>
</div>
</div>
<script>
const prospekId = document.getElementById('prospek_id').value;
async function loadJurusanAndData() {
const token = localStorage.getItem('access_token');
try {
const resJurusan = await fetch('/api/admin/jurusan', {
headers: { 'Authorization': 'Bearer ' + token }
});
const resultJurusan = await resJurusan.json();
const select = document.getElementById('jurusan_id');
select.innerHTML = '<option value="" disabled>Pilih Jurusan Terkait...</option>';
resultJurusan.data.forEach(item => {
select.innerHTML += `<option value="${item.id}">${item.nama_jurusan}</option>`;
});
const resProspek = await fetch(`/api/admin/prospek-kerja/${prospekId}`, {
headers: { 'Authorization': 'Bearer ' + token }
});
const resultProspek = await resProspek.json();
if (resProspek.ok) {
document.getElementById('jurusan_id').value = resultProspek.data.jurusan_id;
document.getElementById('nama_profesi').value = resultProspek.data.nama_profesi;
document.getElementById('range_gaji').value = resultProspek.data.range_gaji || '';
document.getElementById('deskripsi_pekerjaan').value = resultProspek.data.deskripsi_pekerjaan || '';
}
} catch (error) { console.error(error); }
}
document.getElementById('formEditProspek').addEventListener('submit', async (e) => {
e.preventDefault();
const btn = document.getElementById('btnUpdate');
btn.disabled = true;
const payload = {
jurusan_id: document.getElementById('jurusan_id').value,
nama_profesi: document.getElementById('nama_profesi').value,
range_gaji: document.getElementById('range_gaji').value,
deskripsi_pekerjaan: document.getElementById('deskripsi_pekerjaan').value
};
try {
const response = await fetch(`/api/admin/prospek-kerja/${prospekId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('access_token'),
'Accept': 'application/json'
},
body: JSON.stringify(payload)
});
if (response.ok) {
Swal.fire({ icon: 'success', title: 'Berhasil Diperbarui!', confirmButtonColor: '#2D6A4F' })
.then(() => { window.location.href = "{{ route('admin.prospek-kerja') }}"; });
}
} catch (error) { Swal.fire('Error!', 'Gagal koneksi.', 'error'); }
finally { btn.disabled = false; }
});
document.addEventListener('DOMContentLoaded', loadJurusanAndData);
</script>
@endsection

View File

@ -0,0 +1,166 @@
@extends('admin.layout')
@section('content')
<div class="mb-4">
<h4 class="fw-bold text-dark">Riwayat Prediksi Jurusan</h4>
<p class="text-muted small">Hasil prediksi algoritma Random Forest berdasarkan profil siswa.</p>
</div>
<div class="card border-0 shadow-sm rounded-4">
<div class="card-body p-4">
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th class="py-3 px-4">Tanggal</th>
<th class="py-3">Nama Siswa</th>
<th class="py-3">Hasil Rekomendasi</th>
<th class="py-3 text-center">Aksi</th>
</tr>
</thead>
<tbody id="table-riwayat">
<tr>
<td colspan="4" class="text-center py-4 text-muted">
<i class="fas fa-spinner fa-spin me-2"></i> Memuat data riwayat...
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="modal fade" id="detailModal" tabindex="-1" aria-labelledby="detailModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow">
<div class="modal-header border-bottom-0 pb-0">
<h5 class="modal-title fw-bold" id="detailModalLabel">Detail Prediksi Jurusan</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body p-4" id="modal-body-content">
</div>
<div class="modal-footer border-top-0 pt-0">
<button type="button" class="btn btn-secondary px-4 rounded-pill shadow-none" data-bs-dismiss="modal">Tutup</button>
</div>
</div>
</div>
</div>
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', async function() {
const token = localStorage.getItem('access_token');
// Cek sesi login
if (!token) {
window.location.href = '/login';
return;
}
const tbody = document.getElementById('table-riwayat');
try {
// Memanggil API riwayat
const response = await fetch('/api/admin/riwayat', {
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/json'
}
});
const result = await response.json();
if (result.success) {
const data = result.data;
// Jika data kosong
if (data.length === 0) {
tbody.innerHTML = '<tr><td colspan="4" class="text-center py-4 text-muted">Belum ada riwayat prediksi yang dilakukan siswa.</td></tr>';
return;
}
// Render data riwayat ke dalam tabel
tbody.innerHTML = data.map(item => `
<tr>
<td class="px-4 text-muted">${item.tanggal}</td>
<td class="fw-bold">${item.nama_siswa}</td>
<td><span class="badge bg-success px-3 py-2 rounded-pill shadow-none">${item.hasil}</span></td>
<td class="text-center">
<button class="btn btn-sm btn-light px-3 rounded-pill text-primary fw-medium" onclick="showDetail(${item.id})">
<i class="fas fa-file-alt me-1"></i> Detail
</button>
</td>
</tr>
`).join('');
} else {
tbody.innerHTML = `<tr><td colspan="4" class="text-center py-4 text-danger">Gagal memuat data: ${result.message}</td></tr>`;
}
} catch (error) {
console.error('Error fetching riwayat:', error);
tbody.innerHTML = '<tr><td colspan="4" class="text-center py-4 text-danger">Terjadi kesalahan jaringan atau server.</td></tr>';
}
});
/**
* Mengambil dan menampilkan detail riwayat di dalam Modal
*/
async function showDetail(id) {
const token = localStorage.getItem('access_token');
const modalBody = document.getElementById('modal-body-content');
// Tampilkan modal dengan state loading
const detailModal = new bootstrap.Modal(document.getElementById('detailModal'));
detailModal.show();
modalBody.innerHTML = `
<div class="text-center py-4">
<i class="fas fa-spinner fa-spin text-primary fs-3 mb-2"></i>
<p class="text-muted m-0">Mengambil detail...</p>
</div>
`;
try {
// Memanggil API detail riwayat
const response = await fetch(`/api/admin/riwayat/${id}`, {
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/json'
}
});
const result = await response.json();
if (result.success) {
const data = result.data;
// Render detail ke dalam modal
modalBody.innerHTML = `
<div class="mb-3 border-bottom pb-2">
<span class="text-muted d-block small mb-1">Nama Lengkap Siswa</span>
<span class="fw-bold text-dark fs-5">${data.nama_siswa}</span>
</div>
<div class="mb-3 border-bottom pb-2">
<span class="text-muted d-block small mb-1">Email Siswa</span>
<span class="text-dark">${data.email_siswa}</span>
</div>
<div class="mb-3 border-bottom pb-2">
<span class="text-muted d-block small mb-1">Waktu Prediksi Dibuat</span>
<span class="text-dark">${data.tanggal}</span>
</div>
<div class="mb-2">
<span class="text-muted d-block small mb-2">Hasil Rekomendasi Random Forest</span>
<span class="badge bg-success px-3 py-2 rounded-pill fs-6 shadow-none">${data.hasil_rekomendasi}</span>
</div>
`;
} else {
modalBody.innerHTML = `<div class="alert alert-danger m-0">Gagal memuat detail: ${result.message}</div>`;
}
} catch (error) {
console.error('Error fetching detail:', error);
modalBody.innerHTML = `<div class="alert alert-danger m-0">Terjadi kesalahan jaringan saat mengambil detail.</div>`;
}
}
</script>
@endpush
@endsection

View File

@ -0,0 +1,152 @@
@extends('admin.layout')
@section('content')
<link rel="stylesheet" href="{{ asset('css/fitur-admin.css') }}">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<div class="dashboard-section">
<div class="mb-4 d-flex justify-content-between align-items-center">
<div>
<h4 class="fw-bold text-dark">Manajemen Data Siswa</h4>
<p class="text-muted small">Daftar siswa SMAN 4 Jember yang terdaftar di sistem.</p>
</div>
<a href="{{ route('admin.siswa.create') }}" class="btn text-white px-4 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">
<i class="fas fa-plus me-2"></i>Tambah Siswa
</a>
</div>
<div class="table-container-modern shadow-sm">
<div class="table-responsive">
<table class="table table-modern mb-0">
<thead>
<tr>
<th class="text-center" width="80">ID</th>
<th>Nama Lengkap</th>
<th>NISN</th>
<th class="text-center">Kelas</th>
<th class="text-center" width="150">Opsi</th>
</tr>
</thead>
<tbody id="siswa-table-body">
</tbody>
</table>
</div>
<div class="pagination-container">
<div class="text-muted small" id="pagination-info">
Menampilkan 0 sampai 0 dari 0 data
</div>
<div class="d-flex gap-1" id="pagination-controls">
</div>
</div>
</div>
</div>
<script>
async function fetchSiswa(page = 1) {
const token = localStorage.getItem('access_token');
const tbody = document.getElementById('siswa-table-body');
tbody.innerHTML = '<tr><td colspan="5" class="text-center py-5">Memuat data...</td></tr>';
try {
const response = await fetch(`/api/admin/siswa?page=${page}`, {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + token,
'Accept': 'application/json'
}
});
const result = await response.json();
tbody.innerHTML = '';
if (!result.data || result.data.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" class="text-center py-5">Belum ada data siswa.</td></tr>';
return;
}
result.data.forEach((item) => {
const row = `
<tr>
<td class="text-center fw-bold text-muted">${item.id_siswa}</td>
<td class="fw-bold text-dark">${item.nama_lengkap}</td>
<td class="text-muted">${item.nisn}</td>
<td class="text-center">
<span class="badge rounded-pill badge-kelas">${item.kelas}</span>
</td>
<td class="text-center">
<div class="opsi-wrapper">
<a href="/admin/siswa/${item.id}/edit" class="btn-action btn-edit" title="Edit">
<i class="fas fa-edit"></i>
</a>
<button onclick="deleteSiswa(${item.id})" class="btn-action btn-delete" title="Hapus">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>`;
tbody.innerHTML += row;
});
document.getElementById('pagination-info').innerText =
`Menampilkan ${result.from || 0} sampai ${result.to || 0} dari ${result.total || 0} data`;
renderPagination(result);
} catch (error) {
tbody.innerHTML = '<tr><td colspan="5" class="text-center py-5 text-danger">Gagal memuat data.</td></tr>';
}
}
function renderPagination(data) {
const controls = document.getElementById('pagination-controls');
controls.innerHTML = '';
const prevBtn = document.createElement('button');
prevBtn.className = 'page-link-custom';
prevBtn.innerHTML = '&laquo; Previous';
prevBtn.disabled = data.current_page === 1;
prevBtn.onclick = () => fetchSiswa(data.current_page - 1);
controls.appendChild(prevBtn);
const pageBtn = document.createElement('button');
pageBtn.className = 'page-link-custom active';
pageBtn.innerText = data.current_page || 1;
controls.appendChild(pageBtn);
const nextBtn = document.createElement('button');
nextBtn.className = 'page-link-custom';
nextBtn.innerHTML = 'Next &raquo;';
nextBtn.disabled = data.current_page === data.last_page;
nextBtn.onclick = () => fetchSiswa(data.current_page + 1);
controls.appendChild(nextBtn);
}
async function deleteSiswa(id) {
Swal.fire({
title: 'Hapus data?',
text: "Data akan dihapus permanen!",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#ef4444',
confirmButtonText: 'Ya, Hapus'
}).then(async (result) => {
if (result.isConfirmed) {
const token = localStorage.getItem('access_token');
try {
const response = await fetch('/api/admin/siswa/' + id, {
method: 'DELETE',
headers: { 'Authorization': 'Bearer ' + token, 'Accept': 'application/json' }
});
if (response.ok) {
Swal.fire('Berhasil!', 'Data telah dihapus.', 'success');
fetchSiswa();
}
} catch (error) { Swal.fire('Error!', 'Koneksi gagal.', 'error'); }
}
});
}
document.addEventListener('DOMContentLoaded', () => fetchSiswa(1));
</script>
@endsection

View File

@ -0,0 +1,104 @@
@extends('admin.layout')
@section('content')
<link rel="stylesheet" href="{{ asset('css/fitur-admin.css') }}">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<div class="dashboard-section d-flex justify-content-center">
<div style="width: 100%; max-width: 700px;">
<div class="mb-4 text-center">
<h4 class="fw-bold text-dark">Tambah Siswa Baru</h4>
<p class="text-muted small text-uppercase fw-bold" style="letter-spacing: 1px;">Sistem Rekomendasi SMAN 4 Jember</p>
</div>
<div class="card border-0 shadow-sm rounded-4">
<div class="card-body p-5">
<form id="createSiswaForm">
<div class="row g-4">
<div class="col-md-12 form-group-custom">
<label>Nama Lengkap</label>
<input type="text" id="nama_lengkap" class="form-control input-custom" required placeholder="Masukkan nama lengkap siswa">
</div>
<div class="col-md-12 form-group-custom">
<label>NISN</label>
<input type="text" id="nisn" class="form-control input-custom" required placeholder="Masukkan 10 digit NISN">
</div>
<div class="col-md-12 form-group-custom">
<label>Kelas</label>
<input type="text" id="kelas" class="form-control input-custom" required placeholder="Contoh: XII IPA 1">
</div>
<div class="col-md-12 form-group-custom">
<label>Peminatan</label>
<select id="peminatan" class="form-select input-custom">
<option value="" disabled selected>Pilih Peminatan</option>
<option value="Saintek">Saintek</option>
<option value="Soshum">Soshum</option>
</select>
</div>
<div class="col-md-12 form-group-custom">
<label>User ID (Sesuai Akun Login)</label>
<input type="number" id="user_id" class="form-control input-custom" required placeholder="Contoh: 1">
</div>
<div class="col-md-12 form-group-custom">
<label>Jurusan ID</label>
<input type="number" id="jurusan_id" class="form-control input-custom" required placeholder="Contoh: 1">
</div>
</div>
<div class="mt-5 d-flex gap-2 justify-content-end">
<button type="submit" id="btnSave" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">Simpan Data</button>
<a href="{{ route('admin.siswa') }}" class="btn btn-light px-4" style="border-radius: 10px;">Batal</a>
</div>
</form>
</div>
</div>
</div>
</div>
<script>
document.getElementById('createSiswaForm').addEventListener('submit', async (e) => {
e.preventDefault();
const btn = document.getElementById('btnSave');
btn.disabled = true;
const payload = {
user_id: document.getElementById('user_id').value,
jurusan_id: document.getElementById('jurusan_id').value,
nama_lengkap: document.getElementById('nama_lengkap').value,
nisn: document.getElementById('nisn').value,
kelas: document.getElementById('kelas').value,
peminatan: document.getElementById('peminatan').value,
};
try {
const response = await fetch('/api/admin/siswa', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('access_token'),
'Accept': 'application/json'
},
body: JSON.stringify(payload)
});
const result = await response.json();
if (response.ok) {
Swal.fire({ icon: 'success', title: 'Berhasil!', text: 'Data siswa ditambahkan.', confirmButtonColor: '#2D7A47' })
.then(() => { window.location.href = "{{ route('admin.siswa') }}"; });
} else {
Swal.fire('Gagal!', result.message || 'Cek kembali data Anda.', 'warning');
}
} catch (error) {
Swal.fire('Error!', 'Gagal menghubungi server.', 'error');
} finally {
btn.disabled = false;
}
});
</script>
@endsection

View File

@ -0,0 +1,122 @@
@extends('admin.layout')
@section('content')
<link rel="stylesheet" href="{{ asset('css/fitur-admin.css') }}">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<div class="dashboard-section d-flex justify-content-center">
<div style="width: 100%; max-width: 700px;">
<div class="mb-4 text-center">
<h4 class="fw-bold text-dark">Edit Data Siswa</h4>
<p id="display_id_siswa" class="text-muted small fw-bold">Memuat data...</p>
</div>
<div class="card border-0 shadow-sm rounded-4">
<div class="card-body p-5">
<form id="editSiswaForm">
<input type="hidden" id="siswa_id_db" value="{{ $id }}">
<div class="row g-4">
<div class="col-md-12 form-group-custom">
<label>Nama Lengkap</label>
<input type="text" id="nama_lengkap" class="form-control input-custom" required>
</div>
<div class="col-md-12 form-group-custom">
<label>NISN</label>
<input type="text" id="nisn" class="form-control input-custom" required>
</div>
<div class="col-md-12 form-group-custom">
<label>Kelas</label>
<input type="text" id="kelas" class="form-control input-custom" required>
</div>
<div class="col-md-12 form-group-custom">
<label>Peminatan</label>
<select id="peminatan" class="form-select input-custom">
<option value="Saintek">Saintek</option>
<option value="Soshum">Soshum</option>
</select>
</div>
</div>
<div class="mt-5 d-flex gap-2 justify-content-end">
<button type="submit" id="btnUpdate" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">Perbarui Data</button>
<a href="{{ route('admin.siswa') }}" class="btn btn-light px-4" style="border-radius: 10px;">Batal</a>
</div>
</form>
</div>
</div>
</div>
</div>
<script>
const dbId = document.getElementById('siswa_id_db').value;
// Mengambil data lama untuk ditampilkan di form
async function loadSiswa() {
try {
const response = await fetch(`/api/admin/siswa/${dbId}`, {
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('access_token'),
'Accept': 'application/json'
}
});
const result = await response.json();
if (response.ok) {
document.getElementById('nama_lengkap').value = result.data.nama_lengkap;
document.getElementById('nisn').value = result.data.nisn;
document.getElementById('kelas').value = result.data.kelas;
document.getElementById('peminatan').value = result.data.peminatan;
document.getElementById('display_id_siswa').innerText = `EDITING: ${result.data.id_siswa}`;
} else {
Swal.fire('Gagal!', 'Data tidak ditemukan.', 'warning');
}
} catch (error) {
Swal.fire('Error!', 'Gagal mengambil data dari server.', 'error');
}
}
// Mengirim data perbaikan ke API
document.getElementById('editSiswaForm').addEventListener('submit', async (e) => {
e.preventDefault();
const btn = document.getElementById('btnUpdate');
btn.disabled = true;
const payload = {
nama_lengkap: document.getElementById('nama_lengkap').value,
nisn: document.getElementById('nisn').value,
kelas: document.getElementById('kelas').value,
peminatan: document.getElementById('peminatan').value
};
try {
const response = await fetch(`/api/admin/siswa/${dbId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('access_token'),
'Accept': 'application/json'
},
body: JSON.stringify(payload)
});
if (response.ok) {
Swal.fire({ icon: 'success', title: 'Berhasil Diperbarui!', text: 'Data siswa telah diperbarui.', confirmButtonColor: '#2D7A47' })
.then(() => { window.location.href = "{{ route('admin.siswa') }}"; });
} else {
const result = await response.json();
Swal.fire('Gagal!', result.message || 'Cek inputan.', 'warning');
}
} catch (error) {
Swal.fire('Error!', 'Kesalahan koneksi server.', 'error');
} finally {
btn.disabled = false;
}
});
// Jalankan load data saat halaman siap
document.addEventListener('DOMContentLoaded', loadSiswa);
</script>
@endsection

View File

@ -0,0 +1,129 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login ASGARD - SMAN 4 Jember</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="{{ asset('css/auth.css') }}" rel="stylesheet">
</head>
<body class="auth-page px-6">
<div class="login-container">
<div class="login-visual">
<div class="visual-pattern"></div>
<div class="relative z-10">
<div class="mb-12">
<span class="badge-ai">AI TECHNOLOGY</span>
</div>
<h2 class="text-5xl font-extrabold italic tracking-tighter mb-6 leading-tight">
Tentukan Jalur <br> Karirmu Bersama <br> <span class="text-green-400">ASGARD.</span>
</h2>
<p class="text-green-100/60 font-medium leading-relaxed max-w-sm mb-12">
Sistem bimbingan cerdas SMAN 4 Jember membantu memetakan masa depanmu melalui kekuatan data.
</p>
<div class="flex items-center space-x-4 opacity-50">
<div class="w-10 h-10 rounded-full border border-white flex items-center justify-center font-bold">S4</div>
<div class="text-[10px] font-bold uppercase tracking-widest">Official Academic System <br> SMAN 4 Jember</div>
</div>
</div>
</div>
<div class="login-form-area">
<div class="mb-10">
<h3 class="text-3xl font-black text-slate-900 tracking-tighter">AI Login <span class="text-green-600"></span></h3>
<p class="text-slate-400 font-bold text-[10px] uppercase tracking-widest mt-2">Sistem Rekomendasi Jurusan</p>
</div>
<form id="loginForm" class="space-y-6">
@csrf
<div>
<label class="block text-[10px] font-black uppercase tracking-widest text-slate-400 mb-3 ml-1">Username / ID Siswa</label>
<input type="text" id="username" name="username" required
class="input-modern w-full text-slate-900"
placeholder="Masukkan ID Siswa">
</div>
<div>
<div class="flex justify-between items-center mb-3 px-1">
<label class="block text-[10px] font-black uppercase tracking-widest text-slate-400">Password</label>
<a href="#" class="text-[10px] font-black text-green-600 hover:text-green-800 uppercase tracking-widest">Bantuan</a>
</div>
<input type="password" id="password" name="password" required
class="input-modern w-full text-slate-900"
placeholder="••••••••">
<p id="error-message" class="hidden text-red-500 text-[11px] font-bold mt-2 ml-1 italic"></p>
</div>
<div class="pt-4 flex flex-col gap-4">
<button type="submit" id="btnLogin" class="btn-login w-full">
Masuk Ke ASGARD
</button>
<a href="{{ route('landingpage') }}" class="btn-login w-full text-center flex items-center justify-center">
Kembali ke Beranda
</a>
</div>
</form>
<div class="mt-12 text-center">
<p class="text-[11px] text-slate-400 font-bold uppercase tracking-widest">
Punya kendala akses?
<a href="#" class="text-green-600 hover:text-green-800 ml-1 underline">Hubungi Guru BK</a>
</p>
</div>
</div>
</div>
<script>
document.getElementById('loginForm').addEventListener('submit', async function(e) {
e.preventDefault();
const btn = document.getElementById('btnLogin');
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const errorElement = document.getElementById('error-message');
errorElement.classList.add('hidden');
btn.disabled = true;
btn.innerText = 'Memproses...';
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
username: username,
password: password
})
});
const data = await response.json();
if (response.ok) {
localStorage.setItem('access_token', data.access_token);
localStorage.setItem('user_role', data.role);
if (data.role === 'admin') {
window.location.href = "{{ route('admin.dashboard') }}";
} else {
window.location.href = "{{ route('student.profile') }}";
}
} else {
errorElement.innerText = data.message || 'Username atau password salah.';
errorElement.classList.remove('hidden');
}
} catch (error) {
console.error('Error:', error);
errorElement.innerText = 'Koneksi gagal. Silakan coba lagi.';
errorElement.classList.remove('hidden');
} finally {
btn.disabled = false;
btn.innerText = 'Masuk Ke ASGARD';
}
});
</script>
</body>
</html>

View File

@ -0,0 +1,179 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ASGARD - Rekomendasi Jurusan SMAN 4 Jember</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="{{ asset('css/landingpage.css') }}" rel="stylesheet">
<link href="https://unpkg.com/aos@2.3.1/dist/aos.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css" />
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;700;800&display=swap" rel="stylesheet">
<style>
body { font-family: 'Plus Jakarta Sans', sans-serif; }
.text-gradient {
background: linear-gradient(to right, #10b981, #059669);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
</style>
</head>
<body x-data="{ atTop: true }" @scroll.window="atTop = (window.pageYOffset > 50 ? false : true)" class="flex flex-col min-h-screen">
<nav class="fixed w-full z-50 transition-all duration-500 px-6 py-4" :class="atTop ? 'bg-transparent' : 'bg-white/90 backdrop-blur-xl shadow-lg border-b border-green-100'">
<div class="max-w-7xl mx-auto flex items-center justify-between">
<div class="flex items-center space-x-2">
<span class="font-black text-2xl tracking-tighter text-emerald-900 italic">ASGARD<span class="text-green-500">.</span></span>
</div>
<div class="hidden md:flex space-x-10 text-[11px] font-bold uppercase tracking-widest text-emerald-900">
<a href="#tentang" class="hover:text-green-600 transition">Tentang</a>
<a href="#fitur" class="hover:text-green-600 transition">Variabel</a>
<a href="#faq" class="hover:text-green-600 transition">FAQ</a>
</div>
<a href="{{ route('login') }}" class="bg-emerald-900 text-white px-8 py-3 rounded-full font-bold hover:bg-black hover:scale-105 transition-all">LOGIN</a>
</div>
</nav>
<main class="flex-grow">
<header class="pt-32 pb-20 overflow-hidden">
<div class="max-w-7xl mx-auto px-6">
<div class="swiper heroSwiper" data-aos="fade-up">
<div class="swiper-wrapper">
<div class="swiper-slide text-center py-12">
<h1 class="text-6xl md:text-8xl font-black text-emerald-950 mb-6 leading-none tracking-tighter">
Wujudkan Impianmu <br> Bersama <span class="text-gradient">ASGARD.</span>
</h1>
<p class="max-w-2xl mx-auto text-emerald-900/60 text-lg mb-10 font-medium">
Sistem cerdas penentu masa depan siswa SMAN 4 Jember.
</p>
<a href="{{ route('login') }}" class="bg-green-600 text-white px-12 py-5 rounded-2xl font-bold uppercase text-xs tracking-widest shadow-2xl active:scale-95 transition-all">Mulai Temukan Jurusan</a>
</div>
<div class="swiper-slide text-center py-12">
<h2 class="text-5xl font-black text-emerald-950 mb-6">Kenapa ASGARD?</h2>
<p class="max-w-xl mx-auto text-emerald-900/60 mb-8 text-lg italic leading-relaxed">
"Membantu 87% mahasiswa yang sering merasa salah jurusan dengan bimbingan berbasis data objektif."
</p>
</div>
</div>
<div class="swiper-pagination"></div>
</div>
</div>
</header>
<section id="tentang" class="py-24 px-6">
<div class="max-w-7xl mx-auto grid md:grid-cols-2 gap-16 items-center">
<div data-aos="fade-right">
<h2 class="text-4xl font-black text-emerald-950 mb-8 uppercase italic tracking-tighter">Apa itu ASGARD?</h2>
<p class="text-slate-600 leading-relaxed text-lg mb-6">
<strong>ASGARD</strong> adalah singkatan dari <em>AI System for Graduation & Academic Recommendation</em>. Ini adalah platform rekomendasi jurusan kuliah yang dirancang khusus untuk memandu siswa SMAN 4 Jember menemukan jalur karir terbaik.
</p>
<p class="text-slate-600 leading-relaxed text-lg font-medium border-l-4 border-green-500 pl-6">
Sistem ini tidak hanya menebak, tapi menghitung potensi diri berdasarkan variabel nyata yang ada pada profilmu.
</p>
</div>
<div class="flex justify-center" data-aos="zoom-in">
<img src="https://illustrations.popsy.co/emerald/student-going-to-school.svg" alt="Student" class="w-full max-w-sm animate-float">
</div>
</div>
</section>
<section id="fitur" class="py-24 px-6 bg-green-50/50">
<div class="max-w-7xl mx-auto">
<div class="text-center mb-16">
<h2 class="text-4xl font-black text-emerald-950 italic uppercase">Variabel Analisis Kami</h2>
<p class="text-emerald-900/50 font-bold tracking-widest text-[10px] mt-2 uppercase">Dasar Prediksi Rekomendasi</p>
</div>
<div class="grid md:grid-cols-4 gap-6">
<div class="bg-white rounded-3xl p-10 flex flex-col justify-between h-80 shadow-sm border border-emerald-100" data-aos="fade-up">
<span class="text-4xl">📝</span>
<div>
<h4 class="font-black text-emerald-950 mb-2 uppercase">Nilai Rapor</h4>
<p class="text-sm text-slate-500 leading-relaxed">Analisis performa akademikmu selama 5 semester terakhir.</p>
</div>
</div>
<div class="bg-emerald-900 rounded-3xl p-10 flex flex-col justify-between h-80 text-white shadow-xl" data-aos="fade-up" data-aos-delay="100">
<span class="text-4xl">🧠</span>
<div>
<h4 class="font-black mb-2 uppercase text-green-400">Minat & Bakat</h4>
<p class="text-sm text-emerald-100/60 leading-relaxed">Memetakan kecenderungan bakatmu melalui kuesioner psikologi.</p>
</div>
</div>
<div class="bg-yellow-400 rounded-3xl p-10 flex flex-col justify-between h-80 shadow-sm" data-aos="fade-up" data-aos-delay="200">
<span class="text-4xl">💰</span>
<div>
<h4 class="font-black text-emerald-900 mb-2 uppercase">Kondisi Ekonomi</h4>
<p class="text-sm text-emerald-900/60 leading-relaxed font-bold">Menyesuaikan pilihan jurusan dengan kesiapan finansial keluarga.</p>
</div>
</div>
<div class="bg-white rounded-3xl p-10 flex flex-col justify-between h-80 shadow-sm border border-emerald-100" data-aos="fade-up" data-aos-delay="300">
<span class="text-4xl">🎓</span>
<div>
<h4 class="font-black text-emerald-950 mb-2 uppercase">Data Alumni</h4>
<p class="text-sm text-slate-500 leading-relaxed">Dicocokkan dengan tingkat keberhasilan alumni SMAPA di berbagai PTN.</p>
</div>
</div>
</div>
</div>
</section>
<section id="faq" class="py-24 px-6">
<div class="max-w-3xl mx-auto" x-data="{ active: 1 }">
<h2 class="text-center text-4xl font-black text-emerald-950 mb-16 italic">Pertanyaan Umum</h2>
<div class="space-y-4">
<div class="bg-white rounded-3xl border border-green-100 overflow-hidden shadow-sm">
<button @click="active = (active === 1 ? null : 1)" class="w-full px-8 py-6 text-left font-bold text-emerald-900 flex justify-between items-center">
<span>Bagaimana cara kerja ASGARD?</span>
<svg class="w-5 h-5 transition-transform" :class="active === 1 ? 'rotate-180' : ''" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M19 9l-7 7-7-7"></path></svg>
</button>
<div x-show="active === 1" class="px-8 pb-6 text-slate-500 leading-relaxed">ASGARD memproses semua variabel profilmu menggunakan algoritma AI untuk menghasilkan persentase kecocokan jurusan yang paling optimal.</div>
</div>
</div>
</div>
</section>
</main>
<footer class="bg-[#062C22] text-white py-16 px-6">
<div class="max-w-7xl mx-auto grid grid-cols-1 md:grid-cols-3 gap-12">
<div class="space-y-6">
<div class="flex items-center gap-4">
<span class="font-black text-2xl tracking-tighter uppercase italic">ASGARD.</span>
</div>
<p class="text-slate-300 text-sm leading-relaxed max-w-xs">
Sistem pakar rekomendasi jurusan kuliah SMAN 4 Jember berbasis data-driven.
</p>
</div>
<div>
<h5 class="font-bold text-xs uppercase tracking-widest text-emerald-400 mb-6 italic">Informasi</h5>
<ul class="space-y-4 text-sm text-slate-300">
<li><a href="{{ route('panduan') }}" class="hover:text-white transition">Panduan Sistem</a></li>
<li><a href="#" class="hover:text-white transition">Kebijakan Privasi</a></li>
</ul>
</div>
<div>
<h5 class="font-bold text-xs uppercase tracking-widest text-emerald-400 mb-6 italic">Sekolah</h5>
<ul class="space-y-4 text-sm text-slate-300">
<li>SMAN 4 Jember</li>
<li>Jl. Wolter Monginsidi No.24</li>
<li>Jember, Jawa Timur</li>
</ul>
</div>
</div>
<div class="max-w-7xl mx-auto mt-16 pt-8 border-t border-white/10 text-center">
<p class="text-[10px] font-medium uppercase tracking-[0.5em] text-slate-400">© 2026 GALANG SEFIAN ADJI - POLITEKNIK NEGERI JEMBER</p>
</div>
</footer>
<script src="https://unpkg.com/aos@2.3.1/dist/aos.js"></script>
<script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js"></script>
<script>
AOS.init({ duration: 1000, once: true });
new Swiper(".heroSwiper", {
pagination: { el: ".swiper-pagination", clickable: true },
autoplay: { delay: 4000 },
loop: true,
});
</script>
</body>
</html>

View File

@ -0,0 +1,211 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Panduan Sistem - ASGARD</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;700;800&display=swap" rel="stylesheet">
<style>
body { font-family: 'Plus Jakarta Sans', sans-serif; background-color: #f8fafc; }
</style>
</head>
<body class="flex flex-col min-h-screen">
<nav class="bg-white border-b border-slate-100 px-6 py-4 mb-10 relative z-50">
<div class="max-w-7xl mx-auto flex justify-between items-center">
<a href="{{ url('/') }}" class="beranda-link flex items-center gap-4 group">
<div class="w-10 h-10 bg-emerald-100 rounded-xl flex items-center justify-center text-emerald-600 font-bold transition-all duration-300 group-hover:bg-emerald-600 group-hover:text-white">AS</div>
<span class="font-black text-xl tracking-tighter text-emerald-950 uppercase italic">ASGARD.</span>
</a>
<div class="flex items-center gap-8">
<div class="hidden md:flex gap-8 text-[11px] font-bold uppercase tracking-widest text-slate-400">
<a href="{{ url('/') }}" class="beranda-link hover:text-emerald-600 transition-colors">Beranda</a>
<a href="{{ route('panduan') }}" class="text-emerald-600">Panduan</a>
</div>
<a href="{{ route('login') }}" id="login-btn" class="bg-emerald-600 text-white text-[11px] font-bold uppercase tracking-widest px-6 py-3 rounded-xl hover:bg-emerald-700 hover:-translate-y-0.5 transition-all duration-300 shadow-lg shadow-emerald-600/30">
Masuk Sistem
</a>
</div>
</div>
</nav>
<div class="max-w-4xl mx-auto px-6 flex-grow mb-20 w-full relative z-10">
<div class="mb-12 text-center md:text-left">
<div class="inline-flex items-center gap-2 bg-slate-50 text-emerald-700 px-4 py-2 rounded-full font-bold text-xs uppercase tracking-widest mb-4 border border-slate-200">
<span>📚</span> Informasi Terbuka
</div>
<h1 class="text-4xl md:text-5xl font-black text-slate-900 tracking-tight mb-4">Panduan Penggunaan ASGARD</h1>
<p class="text-slate-500 text-lg leading-relaxed">
ASGARD hadir untuk membantu memetakan karir masa depan siswa SMAN 4 Jember. Pelajari 9 langkah alur kerjanya di bawah ini.
</p>
</div>
<div class="space-y-5">
<div class="group bg-white border border-slate-200 p-8 rounded-3xl flex flex-col md:flex-row gap-6 transition-all duration-300 hover:border-emerald-300 hover:shadow-xl hover:shadow-emerald-900/5 hover:-translate-y-1 cursor-default">
<div class="flex-shrink-0">
<div class="w-14 h-14 bg-slate-50 border border-slate-100 text-slate-400 rounded-2xl flex items-center justify-center text-2xl font-black transition-all duration-300 group-hover:bg-emerald-600 group-hover:border-emerald-600 group-hover:text-white group-hover:shadow-lg group-hover:shadow-emerald-600/30">1</div>
</div>
<div>
<h3 class="text-xl font-bold text-slate-800 mb-2 transition-colors duration-300 group-hover:text-emerald-800">Pendataan oleh Guru BK (Admin)</h3>
<p class="text-slate-500 leading-relaxed transition-colors duration-300 group-hover:text-slate-600">
Siswa tidak perlu melakukan registrasi akun secara mandiri. Data dasar siswa dan pembuatan akun akan dilakukan sepenuhnya oleh Guru BK (Admin) ke dalam sistem untuk menjaga validitas data.
</p>
</div>
</div>
<div class="group bg-white border border-slate-200 p-8 rounded-3xl flex flex-col md:flex-row gap-6 transition-all duration-300 hover:border-emerald-300 hover:shadow-xl hover:shadow-emerald-900/5 hover:-translate-y-1 cursor-default">
<div class="flex-shrink-0">
<div class="w-14 h-14 bg-slate-50 border border-slate-100 text-slate-400 rounded-2xl flex items-center justify-center text-2xl font-black transition-all duration-300 group-hover:bg-emerald-600 group-hover:border-emerald-600 group-hover:text-white group-hover:shadow-lg group-hover:shadow-emerald-600/30">2</div>
</div>
<div>
<h3 class="text-xl font-bold text-slate-800 mb-2 transition-colors duration-300 group-hover:text-emerald-800">Akses & Login Akun Siswa</h3>
<p class="text-slate-500 leading-relaxed transition-colors duration-300 group-hover:text-slate-600">
Siswa dapat masuk ke sistem melalui menu <strong>Login</strong> menggunakan kredensial (seperti email/NISN) dan kata sandi yang telah diberikan oleh Guru BK.
</p>
</div>
</div>
<div class="group bg-white border border-slate-200 p-8 rounded-3xl flex flex-col md:flex-row gap-6 transition-all duration-300 hover:border-emerald-300 hover:shadow-xl hover:shadow-emerald-900/5 hover:-translate-y-1 cursor-default">
<div class="flex-shrink-0">
<div class="w-14 h-14 bg-slate-50 border border-slate-100 text-slate-400 rounded-2xl flex items-center justify-center text-2xl font-black transition-all duration-300 group-hover:bg-emerald-600 group-hover:border-emerald-600 group-hover:text-white group-hover:shadow-lg group-hover:shadow-emerald-600/30">3</div>
</div>
<div>
<h3 class="text-xl font-bold text-slate-800 mb-2 transition-colors duration-300 group-hover:text-emerald-800">Pengisian Data Ekonomi Keluarga</h3>
<p class="text-slate-500 leading-relaxed transition-colors duration-300 group-hover:text-slate-600">
Langkah pertama setelah masuk adalah menginput rentang penghasilan (gaji) orang tua. Sistem membutuhkan data ini untuk memetakan kelompok kemampuan ekonomi agar rekomendasi jurusan sesuai dengan kesiapan finansial.
</p>
</div>
</div>
<div class="group bg-white border border-slate-200 p-8 rounded-3xl flex flex-col md:flex-row gap-6 transition-all duration-300 hover:border-emerald-300 hover:shadow-xl hover:shadow-emerald-900/5 hover:-translate-y-1 cursor-default">
<div class="flex-shrink-0">
<div class="w-14 h-14 bg-slate-50 border border-slate-100 text-slate-400 rounded-2xl flex items-center justify-center text-2xl font-black transition-all duration-300 group-hover:bg-emerald-600 group-hover:border-emerald-600 group-hover:text-white group-hover:shadow-lg group-hover:shadow-emerald-600/30">4</div>
</div>
<div>
<h3 class="text-xl font-bold text-slate-800 mb-2 transition-colors duration-300 group-hover:text-emerald-800">Pengisian Kuesioner Minat & Bakat</h3>
<p class="text-slate-500 leading-relaxed transition-colors duration-300 group-hover:text-slate-600">
Selanjutnya, siswa diwajibkan untuk mengisi instrumen kuesioner psikologi untuk memetakan kecenderungan minat dan bakat secara jujur. Data ini dijamin kerahasiaannya.
</p>
</div>
</div>
<div class="group bg-white border border-slate-200 p-8 rounded-3xl flex flex-col md:flex-row gap-6 transition-all duration-300 hover:border-emerald-300 hover:shadow-xl hover:shadow-emerald-900/5 hover:-translate-y-1 cursor-default">
<div class="flex-shrink-0">
<div class="w-14 h-14 bg-slate-50 border border-slate-100 text-slate-400 rounded-2xl flex items-center justify-center text-2xl font-black transition-all duration-300 group-hover:bg-emerald-600 group-hover:border-emerald-600 group-hover:text-white group-hover:shadow-lg group-hover:shadow-emerald-600/30">5</div>
</div>
<div>
<h3 class="text-xl font-bold text-slate-800 mb-2 transition-colors duration-300 group-hover:text-emerald-800">Pengisian Form Nilai Rapor</h3>
<p class="text-slate-500 leading-relaxed transition-colors duration-300 group-hover:text-slate-600">
Tahap input terakhir adalah mengisi rata-rata nilai dari 15 mata pelajaran selama semester 1 hingga semester 5 dengan akurat sebagai indikator performa akademik utama.
</p>
</div>
</div>
<div class="group bg-white border border-slate-200 p-8 rounded-3xl flex flex-col md:flex-row gap-6 transition-all duration-300 hover:border-emerald-300 hover:shadow-xl hover:shadow-emerald-900/5 hover:-translate-y-1 cursor-default">
<div class="flex-shrink-0">
<div class="w-14 h-14 bg-slate-50 border border-slate-100 text-slate-400 rounded-2xl flex items-center justify-center text-2xl font-black transition-all duration-300 group-hover:bg-emerald-600 group-hover:border-emerald-600 group-hover:text-white group-hover:shadow-lg group-hover:shadow-emerald-600/30">6</div>
</div>
<div>
<h3 class="text-xl font-bold text-slate-800 mb-2 transition-colors duration-300 group-hover:text-emerald-800 flex items-center gap-2">
Pemrosesan Algoritma Random Forest <span class="text-xl group-hover:animate-pulse">🤖</span>
</h3>
<p class="text-slate-500 leading-relaxed transition-colors duration-300 group-hover:text-slate-600">
Sistem mulai bekerja menganalisis seluruh variabel data. Algoritma <em>Machine Learning</em> akan mencocokkan profilmu dengan database historis alumni SMAPA yang telah teruji tingkat kesuksesannya.
</p>
</div>
</div>
<div class="group bg-white border border-slate-200 p-8 rounded-3xl flex flex-col md:flex-row gap-6 transition-all duration-300 hover:border-emerald-300 hover:shadow-xl hover:shadow-emerald-900/5 hover:-translate-y-1 cursor-default">
<div class="flex-shrink-0">
<div class="w-14 h-14 bg-slate-50 border border-slate-100 text-slate-400 rounded-2xl flex items-center justify-center text-2xl font-black transition-all duration-300 group-hover:bg-emerald-600 group-hover:border-emerald-600 group-hover:text-white group-hover:shadow-lg group-hover:shadow-emerald-600/30">7</div>
</div>
<div>
<h3 class="text-xl font-bold text-slate-800 mb-2 transition-colors duration-300 group-hover:text-emerald-800">Penerbitan Hasil Rekomendasi</h3>
<p class="text-slate-500 leading-relaxed transition-colors duration-300 group-hover:text-slate-600">
Sistem akan menampilkan hasil berupa daftar program studi atau jurusan di Perguruan Tinggi yang memiliki persentase kecocokan paling tinggi dengan profil pribadimu.
</p>
</div>
</div>
<div class="group bg-white border border-slate-200 p-8 rounded-3xl flex flex-col md:flex-row gap-6 transition-all duration-300 hover:border-emerald-300 hover:shadow-xl hover:shadow-emerald-900/5 hover:-translate-y-1 cursor-default">
<div class="flex-shrink-0">
<div class="w-14 h-14 bg-slate-50 border border-slate-100 text-slate-400 rounded-2xl flex items-center justify-center text-2xl font-black transition-all duration-300 group-hover:bg-emerald-600 group-hover:border-emerald-600 group-hover:text-white group-hover:shadow-lg group-hover:shadow-emerald-600/30">8</div>
</div>
<div>
<h3 class="text-xl font-bold text-slate-800 mb-2 transition-colors duration-300 group-hover:text-emerald-800">Eksplorasi Prospek Karir</h3>
<p class="text-slate-500 leading-relaxed transition-colors duration-300 group-hover:text-slate-600">
Kamu tidak hanya mendapat nama jurusan, tapi juga dapat melihat informasi lengkap terkait peluang kerja, jalur profesi, dan relevansi industri dari jurusan yang direkomendasikan tersebut.
</p>
</div>
</div>
<div class="group bg-white border border-slate-200 p-8 rounded-3xl flex flex-col md:flex-row gap-6 transition-all duration-300 hover:border-emerald-300 hover:shadow-xl hover:shadow-emerald-900/5 hover:-translate-y-1 cursor-default">
<div class="flex-shrink-0">
<div class="w-14 h-14 bg-slate-50 border border-slate-100 text-slate-400 rounded-2xl flex items-center justify-center text-2xl font-black transition-all duration-300 group-hover:bg-emerald-600 group-hover:border-emerald-600 group-hover:text-white group-hover:shadow-lg group-hover:shadow-emerald-600/30">9</div>
</div>
<div>
<h3 class="text-xl font-bold text-slate-800 mb-2 transition-colors duration-300 group-hover:text-emerald-800">Konseling & Tindak Lanjut</h3>
<p class="text-slate-500 leading-relaxed transition-colors duration-300 group-hover:text-slate-600">
Hasil prediksi AI ini bersifat sebagai panduan objektif. Langkah terakhir, sangat disarankan untuk membawa hasil laporan sistem ini ke ruang BK untuk sesi konsultasi tatap muka guna memantapkan strategi.
</p>
</div>
</div>
</div>
</div>
<footer class="bg-[#062C22] text-white py-16 px-6 mt-auto w-full relative z-10">
<div class="max-w-7xl mx-auto grid grid-cols-1 md:grid-cols-3 gap-12">
<div class="space-y-6">
<div class="flex items-center gap-4">
<span class="font-black text-2xl tracking-tighter uppercase italic">ASGARD.</span>
</div>
<p class="text-slate-300 text-sm leading-relaxed max-w-xs">
Sistem pakar rekomendasi jurusan kuliah SMAN 4 Jember berbasis data-driven.
</p>
</div>
<div>
<h5 class="font-bold text-xs uppercase tracking-widest text-emerald-400 mb-6">Informasi</h5>
<ul class="space-y-4 text-sm text-slate-300">
<li><a href="{{ route('panduan') }}" class="text-emerald-400 font-bold hover:text-white transition-colors">Panduan Sistem</a></li>
<li><a href="#" class="hover:text-white transition-colors">Kebijakan Privasi</a></li>
</ul>
</div>
<div>
<h5 class="font-bold text-xs uppercase tracking-widest text-emerald-400 mb-6">Sekolah</h5>
<ul class="space-y-4 text-sm text-slate-300">
<li>SMAN 4 Jember</li>
<li>Jl. Wolter Monginsidi No.24</li>
<li>Jember, Jawa Timur</li>
</ul>
</div>
</div>
<div class="max-w-7xl mx-auto mt-16 pt-8 border-t border-white/10 text-center">
<p class="text-[10px] font-medium uppercase tracking-[0.5em] text-slate-400">© 2026 GALANG SEFIAN ADJI - POLITEKNIK NEGERI JEMBER</p>
</div>
</footer>
<script>
document.addEventListener('DOMContentLoaded', function() {
const token = localStorage.getItem('access_token');
const berandaLinks = document.querySelectorAll('.beranda-link');
const loginBtn = document.getElementById('login-btn');
if (token) {
berandaLinks.forEach(link => {
link.href = "{{ route('student.profile') }}";
});
if (loginBtn) {
loginBtn.innerText = "Ke Dashboard →";
loginBtn.href = "{{ route('student.profile') }}";
}
}
});
</script>
</body>
</html>

View File

@ -0,0 +1,256 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ASGARD - Dashboard Siswa</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;700;800&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
body { font-family: 'Plus Jakarta Sans', sans-serif; background-color: #f8fafc; }
.card-shadow { box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.02); }
</style>
</head>
<body class="flex flex-col min-h-screen">
<nav class="bg-white border-b border-slate-100 px-6 py-4 mb-10">
<div class="max-w-7xl mx-auto flex justify-between items-center">
<div class="flex items-center gap-4">
<div class="w-10 h-10 bg-emerald-100 rounded-xl flex items-center justify-center text-emerald-600 font-bold">AS</div>
<span class="font-black text-xl tracking-tighter text-emerald-950 uppercase italic">ASGARD.</span>
</div>
<div class="flex items-center gap-8">
<div class="hidden md:flex gap-8 text-[11px] font-bold uppercase tracking-widest text-slate-400">
<a href="{{ route('student.profile') }}" class="text-emerald-600">Beranda</a>
<a href="{{ route('student.prediction') }}" class="hover:text-emerald-600 transition">Cek Rekomendasi</a>
<a href="#" class="hover:text-emerald-600 transition">Kontak BK</a>
</div>
<div class="flex items-center gap-3">
<div class="flex items-center gap-3 bg-emerald-50 px-4 py-2 rounded-full border border-emerald-100">
<div class="w-2 h-2 bg-emerald-500 rounded-full animate-pulse"></div>
<span class="text-[10px] font-bold text-emerald-700 uppercase">Galang Sefian</span>
</div>
<button onclick="handleLogout()" class="bg-red-50 hover:bg-red-100 text-red-600 border border-red-100 px-4 py-2 rounded-full transition-all">
<span class="text-[10px] font-bold uppercase tracking-widest">Logout</span>
</button>
</div>
</div>
</div>
</nav>
<div class="max-w-7xl mx-auto px-6 flex-grow mb-20">
<header class="mb-10">
<h1 class="text-4xl font-extrabold text-slate-900 tracking-tight">Halo, Siswa SMAPA! 👋</h1>
<p class="text-slate-500 font-medium mt-2">Senang melihat Anda kembali! Mari terus pantau masa depan Anda. </p>
</header>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-10">
<div class="bg-white p-8 rounded-3xl card-shadow border border-slate-100">
<h4 class="text-slate-400 text-xs font-bold uppercase tracking-widest mb-4">Total Aktivitas Rekomendasi</h4>
<p id="stat-total" class="text-6xl font-black text-emerald-600 tracking-tighter">0</p>
<p class="text-[11px] text-slate-400 mt-6 font-medium leading-relaxed">Jumlah total diagnosis & prediksi jurusan kuliah kamu.</p>
</div>
<div class="bg-white p-8 rounded-3xl card-shadow border border-slate-100">
<h4 class="text-slate-400 text-xs font-bold uppercase tracking-widest mb-4">Rekomendasi Terakhir</h4>
<p id="stat-last" class="text-sm font-bold text-slate-700 italic leading-relaxed">Memuat data...</p>
</div>
<div class="bg-white p-8 rounded-3xl card-shadow border border-slate-100">
<h4 class="text-slate-400 text-xs font-bold uppercase tracking-widest mb-4">Aktivitas Terbaru</h4>
<div id="activity-list" class="space-y-3 py-2">
<p class="text-xs text-slate-300 italic">Belum ada aktivitas baru.</p>
</div>
</div>
</div>
<div class="bg-white p-10 rounded-[2.5rem] card-shadow border border-slate-100 mb-10">
<h3 class="text-lg font-black text-emerald-950 mb-8 uppercase italic tracking-wider">Akses Cepat</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<a href="{{ route('student.prediction') }}" class="flex items-center p-6 bg-blue-50 rounded-2xl border border-blue-100 hover:scale-[1.02] transition-all">
<div class="w-12 h-12 bg-blue-500 text-white rounded-xl flex items-center justify-center text-xl mr-5 shadow-lg shadow-blue-200">🤖</div>
<div>
<h4 class="font-black text-blue-900 text-sm italic">Mulai Analisis Jurusan Baru</h4>
<p class="text-[10px] text-blue-600 font-bold mt-1">Dapatkan Rekomendasi AI Sekarang.</p>
</div>
</a>
<a href="#" class="flex items-center p-6 bg-yellow-50 rounded-2xl border border-yellow-100 hover:scale-[1.02] transition-all">
<div class="w-12 h-12 bg-yellow-400 text-white rounded-xl flex items-center justify-center text-xl mr-5 shadow-lg shadow-yellow-200">📊</div>
<div>
<h4 class="font-black text-yellow-900 text-sm italic">Lengkapi Data Rapor</h4>
<p class="text-[10px] text-yellow-700 font-bold mt-1">Input variabel nilai semester 1-5.</p>
</div>
</a>
<a href="#" class="flex items-center p-6 bg-purple-50 rounded-2xl border border-purple-100 hover:scale-[1.02] transition-all">
<div class="w-12 h-12 bg-purple-500 text-white rounded-xl flex items-center justify-center text-xl mr-5 shadow-lg shadow-purple-200">🎓</div>
<div>
<h4 class="font-black text-purple-900 text-sm italic">Riwayat Rekomendasi</h4>
<p class="text-[10px] text-purple-600 font-bold mt-1">Pantau progres pilihan kuliah.</p>
</div>
</a>
</div>
</div>
<div class="bg-white p-10 rounded-[2.5rem] card-shadow border border-slate-100 mb-10">
<h3 class="text-lg font-black text-emerald-950 mb-2 uppercase italic tracking-wider">Tren Prestasi Akademik Anda</h3>
<p class="text-xs text-slate-400 font-medium italic mb-10">Grafik ini menunjukkan perkembangan rata-rata nilai Anda seiring waktu.</p>
<div id="chart-container" class="h-80 w-full">
<canvas id="trendChart"></canvas>
</div>
<p id="chart-placeholder" class="hidden text-center text-slate-400 italic text-sm py-20">Input minimal 2 sesi rekomendasi untuk melihat tren di sini.</p>
</div>
<div class="bg-indigo-600 p-12 rounded-[3.5rem] text-white relative overflow-hidden shadow-2xl mb-16">
<div class="relative z-10">
<div class="flex items-center gap-3 mb-8">
<span class="text-3xl">🎓</span>
<h2 class="text-3xl font-black italic tracking-tighter uppercase leading-none">Masa Depan Anda Prioritas Kami</h2>
</div>
<p class="text-indigo-100 max-w-3xl mb-12 leading-relaxed font-medium">
Tahukah kamu? <span class="font-black text-white italic underline">87% mahasiswa merasa salah jurusan</span>. ASGARD hadir membantu siswa SMAN 4 Jember menghindari risiko tersebut melalui analisis profil yang objektif menggunakan Random Forest.
</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="bg-white/10 backdrop-blur-md p-8 rounded-[2rem] border border-white/20">
<h4 class="font-black text-sm uppercase italic mb-3 tracking-widest">Anti-Blacklist SNBP</h4>
<p class="text-xs text-indigo-100 leading-relaxed italic">Hindari risiko sekolah masuk blacklist perguruan tinggi karena pembatalan kelulusan.</p>
</div>
<div class="bg-white/10 backdrop-blur-md p-8 rounded-[2rem] border border-white/20">
<h4 class="font-black text-sm uppercase italic mb-3 tracking-widest">Prospek Karir AI</h4>
<p class="text-xs text-indigo-100 leading-relaxed italic">Dapatkan informasi jalur karir yang dapat ditempuh sesuai rekomendasi jurusan.</p>
</div>
</div>
</div>
<p class="mt-12 text-center text-[10px] font-black uppercase tracking-[0.2em] italic opacity-80 border-t border-white/10 pt-8 text-white">Penting untuk berkonsultasi dengan Guru BK setelah mendapatkan hasil rekomendasi AI ❤️</p>
</div>
</div>
<footer class="bg-[#062C22] text-white py-16 px-6">
<div class="max-w-7xl mx-auto grid grid-cols-1 md:grid-cols-3 gap-12">
<div class="space-y-6">
<div class="flex items-center gap-4">
<span class="font-black text-2xl tracking-tighter uppercase italic">ASGARD.</span>
</div>
<p class="text-slate-300 text-sm leading-relaxed max-w-xs">
Sistem pakar rekomendasi jurusan kuliah SMAN 4 Jember berbasis data-driven.
</p>
</div>
<div>
<h5 class="font-bold text-xs uppercase tracking-widest text-emerald-400 mb-6">Informasi</h5>
<ul class="space-y-4 text-sm text-slate-300">
<li><a href="{{ route('panduan') }}" class="hover:text-white transition">Panduan Sistem</a></li>
<li><a href="#" class="hover:text-white transition">Kebijakan Privasi</a></li>
</ul>
</div>
<div>
<h5 class="font-bold text-xs uppercase tracking-widest text-emerald-400 mb-6">Sekolah</h5>
<ul class="space-y-4 text-sm text-slate-300">
<li>SMAN 4 Jember</li>
<li>Jl. Wolter Monginsidi No.24</li>
<li>Jember, Jawa Timur</li>
</ul>
</div>
</div>
<div class="max-w-7xl mx-auto mt-16 pt-8 border-t border-white/10 text-center">
<p class="text-[10px] font-medium uppercase tracking-[0.5em] text-slate-400">© 2026 GALANG SEFIAN ADJI - POLITEKNIK NEGERI JEMBER</p>
</div>
</footer>
<script>
async function handleLogout() {
const token = localStorage.getItem('access_token');
if (token) {
try {
await fetch('/api/logout', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/json'
}
});
} catch (error) {
console.error('Logout error:', error);
}
localStorage.removeItem('access_token');
}
window.location.href = '/';
}
document.addEventListener('DOMContentLoaded', async function() {
const token = localStorage.getItem('access_token');
if (!token) {
window.location.href = '/login';
return;
}
try {
const response = await fetch('/api/student/dashboard-stats', {
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/json'
}
});
const result = await response.json();
if (result.success) {
const data = result.data;
// Update Statistik
document.getElementById('stat-total').innerText = data.total_aktivitas;
document.getElementById('stat-last').innerText = data.rekomendasi_terakhir;
// Update List Aktivitas
const listContainer = document.getElementById('activity-list');
if (data.aktivitas_terbaru.length > 0) {
listContainer.innerHTML = data.aktivitas_terbaru.map(act => `
<div class="flex items-center justify-between bg-slate-50 p-3 rounded-xl border border-slate-100">
<div class="flex items-center gap-3">
<span class="text-lg">📋</span>
<span class="text-[11px] font-bold text-slate-700 uppercase tracking-tight">${act.jurusan}</span>
</div>
<span class="text-[9px] font-black text-slate-400 uppercase italic">${act.tanggal}</span>
</div>
`).join('');
}
// Render Chart
const tren = data.tren_nilai;
if (tren.length >= 1) {
const ctx = document.getElementById('trendChart').getContext('2d');
new Chart(ctx, {
type: 'line',
data: {
labels: tren.map(t => t.label),
datasets: [{
label: 'Rata-rata Nilai',
data: tren.map(t => t.nilai),
borderColor: '#10b981',
backgroundColor: 'rgba(16, 185, 129, 0.1)',
fill: true,
tension: 0.4,
pointRadius: 6,
pointBackgroundColor: '#fff',
pointBorderColor: '#10b981',
pointBorderWidth: 3
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
y: { min: 0, max: 100, grid: { display: false } },
x: { grid: { display: false } }
}
}
});
} else {
document.getElementById('trendChart').classList.add('hidden');
document.getElementById('chart-placeholder').classList.remove('hidden');
}
}
} catch (error) {
console.error('Gagal memuat data dashboard:', error);
}
});
</script>
</body>
</html>

View File

@ -0,0 +1,147 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hasil Analisis - ASGARD</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;700;800&display=swap" rel="stylesheet">
<style>
body { font-family: 'Plus Jakarta Sans', sans-serif; background-color: #f8fafc; }
</style>
</head>
<body class="flex flex-col min-h-screen">
<nav class="bg-white border-b border-slate-100 px-6 py-4 mb-10">
<div class="max-w-7xl mx-auto flex justify-between items-center">
<div class="flex items-center gap-4">
<div class="w-10 h-10 bg-emerald-100 rounded-xl flex items-center justify-center text-emerald-600 font-bold">AS</div>
<span class="font-black text-xl tracking-tighter text-emerald-950 uppercase italic">ASGARD.</span>
</div>
<div class="flex items-center gap-3">
<a href="{{ route('student.prediction') }}"
class="text-sm font-semibold text-slate-600 hover:text-emerald-600 transition">
Kembali ke Form
</a>
<button onclick="handleLogout()"
class="bg-red-50 hover:bg-red-100 text-red-600 border border-red-100 px-4 py-2 rounded-full transition-all">
<span class="text-[10px] font-bold uppercase tracking-widest">Logout</span>
</button>
</div>
</div>
</nav>
<main class="flex-grow flex items-center justify-center px-6">
<div class="max-w-xl w-full bg-white p-10 rounded-3xl shadow-xl text-center border border-slate-100 relative overflow-hidden">
<div class="absolute -top-10 -right-10 w-32 h-32 bg-emerald-50 rounded-full blur-2xl"></div>
<div class="absolute -bottom-10 -left-10 w-32 h-32 bg-blue-50 rounded-full blur-2xl"></div>
<div class="relative z-10">
<div class="w-20 h-20 bg-emerald-100 text-emerald-600 rounded-full flex items-center justify-center mx-auto mb-6">
<svg class="w-10 h-10" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
</div>
<h2 class="text-slate-500 font-bold tracking-widest text-sm mb-2 uppercase">Rekomendasi AI Engine</h2>
<h1 class="text-4xl font-extrabold text-slate-800 mb-6 leading-tight">
{{ $riwayat->hasil_rekomendasi_jurusan }}
</h1>
<div class="bg-slate-50 p-6 rounded-2xl mb-8 border border-slate-100 text-left">
<h3 class="font-bold text-slate-800 mb-2">Ringkasan Analisis</h3>
<p class="text-slate-600 text-sm leading-relaxed mb-4">
Berdasarkan perpaduan profil akademik siswa, skor minat bakat RIASEC, dan kondisi ekonomi keluarga,
sistem merekomendasikan jurusan ini sebagai pilihan yang paling sesuai untuk Anda.
</p>
<h3 class="font-bold text-slate-800 mb-2">Peluang Karier</h3>
<p class="text-slate-600 text-sm leading-relaxed">
{{ $prospekKerja ?? 'Informasi prospek karier belum tersedia untuk jurusan ini.' }}
</p>
</div>
<div class="flex flex-col sm:flex-row gap-3 justify-center">
<a href="{{ route('student.prediction') }}"
class="inline-block bg-slate-800 hover:bg-slate-900 text-white font-bold py-3 px-8 rounded-xl transition shadow-lg">
Analisis Ulang
</a>
<a href="{{ route('student.profile') }}"
class="inline-block bg-emerald-600 hover:bg-emerald-700 text-white font-bold py-3 px-8 rounded-xl transition shadow-lg">
Kembali ke Dashboard
</a>
</div>
</div>
</div>
</main>
<footer class="bg-slate-900 text-white mt-20 py-12 px-6">
<div class="max-w-7xl mx-auto flex justify-between items-center text-center">
<p class="text-[10px] font-medium uppercase tracking-[0.2em] text-slate-500 w-full">
© 2026 ASGARD SMAN 4 JEMBER. All Rights Reserved.
</p>
</div>
</footer>
<script>
function getAccessToken() {
return localStorage.getItem('access_token');
}
function getUserRole() {
return localStorage.getItem('user_role');
}
function redirectByRole(role) {
if (role === 'admin') {
window.location.href = "{{ route('admin.dashboard') }}";
return;
}
if (role === 'siswa') {
window.location.href = "{{ route('student.profile') }}";
return;
}
window.location.href = "{{ route('login') }}";
}
async function handleLogout() {
const token = getAccessToken();
if (token) {
try {
await fetch('/api/logout', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/json'
}
});
} catch (error) {
console.error('Logout error:', error);
}
}
localStorage.removeItem('access_token');
localStorage.removeItem('user_role');
window.location.href = "{{ route('login') }}";
}
document.addEventListener('DOMContentLoaded', function () {
const token = getAccessToken();
const role = getUserRole();
if (!token) {
window.location.href = "{{ route('login') }}";
return;
}
if (role !== 'siswa') {
redirectByRole(role);
}
});
</script>
</body>
</html>

View File

@ -0,0 +1,320 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Analisis Rekomendasi - ASGARD SMAN 4 Jember</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;700;800&display=swap" rel="stylesheet">
<meta name="csrf-token" content="{{ csrf_token() }}">
<style>
body { font-family: 'Plus Jakarta Sans', sans-serif; background-color: #f8fafc; }
</style>
</head>
<body class="flex flex-col min-h-screen">
<nav class="bg-white border-b border-slate-100 px-6 py-4 mb-10">
<div class="max-w-7xl mx-auto flex justify-between items-center">
<div class="flex items-center gap-4">
<div class="w-10 h-10 bg-emerald-100 rounded-xl flex items-center justify-center text-emerald-600 font-bold">AS</div>
<span class="font-black text-xl tracking-tighter text-emerald-950 uppercase italic">ASGARD.</span>
</div>
<div class="flex items-center gap-3">
<a href="{{ route('student.profile') }}"
class="text-sm font-semibold text-slate-600 hover:text-emerald-600 transition">
Dashboard
</a>
<button onclick="handleLogout()"
class="bg-red-50 hover:bg-red-100 text-red-600 border border-red-100 px-4 py-2 rounded-full transition-all">
<span class="text-[10px] font-bold uppercase tracking-widest">Logout</span>
</button>
</div>
</div>
</nav>
<div class="max-w-4xl mx-auto px-6 pb-20 w-full">
<h2 class="text-3xl font-extrabold text-slate-800 mb-2">Formulir Rekomendasi Jurusan</h2>
<p class="text-slate-500 mb-8">Lengkapi data nilai dan jawab kuesioner di bawah ini dengan jujur.</p>
<div id="alert-box" class="hidden mb-6 rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700"></div>
<div class="bg-white p-6 rounded-2xl shadow-sm border border-slate-100 mb-8">
<h3 class="font-bold text-lg mb-4 text-emerald-700">1. Data Nilai Rapor (Skala 0-100)</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div><label class="text-sm font-semibold">Agama</label><input type="number" id="agama" min="0" max="100" class="w-full border rounded-lg p-2 mt-1" value="85"></div>
<div><label class="text-sm font-semibold">PKN</label><input type="number" id="pkn" min="0" max="100" class="w-full border rounded-lg p-2 mt-1" value="85"></div>
<div><label class="text-sm font-semibold">Bahasa Indonesia</label><input type="number" id="bahasa_indonesia" min="0" max="100" class="w-full border rounded-lg p-2 mt-1" value="85"></div>
<div><label class="text-sm font-semibold">Bahasa Inggris</label><input type="number" id="bahasa_inggris" min="0" max="100" class="w-full border rounded-lg p-2 mt-1" value="85"></div>
<div><label class="text-sm font-semibold">Matematika</label><input type="number" id="matematika" min="0" max="100" class="w-full border rounded-lg p-2 mt-1" value="85"></div>
<div><label class="text-sm font-semibold">Fisika</label><input type="number" id="fisika" min="0" max="100" class="w-full border rounded-lg p-2 mt-1" value="85"></div>
<div><label class="text-sm font-semibold">Kimia</label><input type="number" id="kimia" min="0" max="100" class="w-full border rounded-lg p-2 mt-1" value="85"></div>
<div><label class="text-sm font-semibold">Biologi</label><input type="number" id="biologi" min="0" max="100" class="w-full border rounded-lg p-2 mt-1" value="85"></div>
<div><label class="text-sm font-semibold">Sejarah</label><input type="number" id="sejarah" min="0" max="100" class="w-full border rounded-lg p-2 mt-1" value="85"></div>
<div><label class="text-sm font-semibold">Geografi</label><input type="number" id="geografi" min="0" max="100" class="w-full border rounded-lg p-2 mt-1" value="85"></div>
<div><label class="text-sm font-semibold">Ekonomi</label><input type="number" id="ekonomi_mapel" min="0" max="100" class="w-full border rounded-lg p-2 mt-1" value="85"></div>
<div><label class="text-sm font-semibold">Sosiologi</label><input type="number" id="sosiologi" min="0" max="100" class="w-full border rounded-lg p-2 mt-1" value="85"></div>
<div><label class="text-sm font-semibold">Seni Budaya</label><input type="number" id="seni_budaya" min="0" max="100" class="w-full border rounded-lg p-2 mt-1" value="85"></div>
<div><label class="text-sm font-semibold">PJOK</label><input type="number" id="pjok" min="0" max="100" class="w-full border rounded-lg p-2 mt-1" value="85"></div>
<div><label class="text-sm font-semibold">Informatika</label><input type="number" id="informatika" min="0" max="100" class="w-full border rounded-lg p-2 mt-1" value="85"></div>
</div>
<div class="mt-4">
<label class="text-sm font-semibold text-slate-700">Rata-rata Gaji Orang Tua (Nominal Rupiah)</label>
<input type="number" id="gaji_ortu" min="0" class="w-full border rounded-lg p-2 mt-1" placeholder="Contoh: 3000000" value="3000000">
</div>
</div>
<div class="bg-white p-6 rounded-2xl shadow-sm border border-slate-100 mb-8">
<h3 class="font-bold text-lg mb-1 text-emerald-700">2. Tes Minat Bakat (RIASEC)</h3>
<p class="text-sm text-slate-500 mb-5">
Centang kegiatan yang Anda sukai. Setiap jawaban "Ya" bernilai 50 poin, dengan maksimum 100 poin per kategori.
</p>
<div class="space-y-4">
<div class="p-4 border border-slate-100 rounded-xl bg-slate-50">
<p class="font-bold text-slate-800 mb-2">Tipe Realistic (R)</p>
<label class="flex items-center gap-3 mb-2"><input type="checkbox" class="w-5 h-5 riasec-chk" data-type="r"> Saya suka memperbaiki barang elektronik atau mesin kendaraan.</label>
<label class="flex items-center gap-3"><input type="checkbox" class="w-5 h-5 riasec-chk" data-type="r"> Saya menyukai kegiatan fisik atau bekerja di luar ruangan.</label>
</div>
<div class="p-4 border border-slate-100 rounded-xl bg-slate-50">
<p class="font-bold text-slate-800 mb-2">Tipe Investigative (I)</p>
<label class="flex items-center gap-3 mb-2"><input type="checkbox" class="w-5 h-5 riasec-chk" data-type="i"> Saya suka meneliti, mengamati, dan memecahkan teka-teki logika.</label>
<label class="flex items-center gap-3"><input type="checkbox" class="w-5 h-5 riasec-chk" data-type="i"> Saya tertarik mendalami teori sains, matematika, atau komputer.</label>
</div>
<div class="p-4 border border-slate-100 rounded-xl bg-slate-50">
<p class="font-bold text-slate-800 mb-2">Tipe Artistic (A)</p>
<label class="flex items-center gap-3 mb-2"><input type="checkbox" class="w-5 h-5 riasec-chk" data-type="a"> Saya senang menggambar, mendesain, atau mengekspresikan seni.</label>
<label class="flex items-center gap-3"><input type="checkbox" class="w-5 h-5 riasec-chk" data-type="a"> Saya lebih suka pekerjaan kreatif yang tidak memiliki aturan kaku.</label>
</div>
<div class="p-4 border border-slate-100 rounded-xl bg-slate-50">
<p class="font-bold text-slate-800 mb-2">Tipe Social (S)</p>
<label class="flex items-center gap-3 mb-2"><input type="checkbox" class="w-5 h-5 riasec-chk" data-type="s"> Saya merasa puas ketika bisa membantu, mengajar, atau merawat orang lain.</label>
<label class="flex items-center gap-3"><input type="checkbox" class="w-5 h-5 riasec-chk" data-type="s"> Saya mudah berempati dan suka bekerja dalam lingkungan masyarakat.</label>
</div>
<div class="p-4 border border-slate-100 rounded-xl bg-slate-50">
<p class="font-bold text-slate-800 mb-2">Tipe Enterprising (E)</p>
<label class="flex items-center gap-3 mb-2"><input type="checkbox" class="w-5 h-5 riasec-chk" data-type="e"> Saya suka memimpin sebuah tim atau bernegosiasi dalam organisasi.</label>
<label class="flex items-center gap-3"><input type="checkbox" class="w-5 h-5 riasec-chk" data-type="e"> Saya pandai meyakinkan orang lain dan tertarik pada dunia bisnis.</label>
</div>
<div class="p-4 border border-slate-100 rounded-xl bg-slate-50">
<p class="font-bold text-slate-800 mb-2">Tipe Conventional (C)</p>
<label class="flex items-center gap-3 mb-2"><input type="checkbox" class="w-5 h-5 riasec-chk" data-type="c"> Saya sangat teliti dan suka mengatur data, dokumen, atau arsip.</label>
<label class="flex items-center gap-3"><input type="checkbox" class="w-5 h-5 riasec-chk" data-type="c"> Saya lebih nyaman bekerja dengan angka, jadwal, dan aturan yang jelas.</label>
</div>
</div>
</div>
<button id="btn-submit" class="w-full bg-emerald-600 hover:bg-emerald-700 text-white p-4 rounded-xl font-bold text-lg transition shadow-lg">
Jalankan AI Engine
</button>
</div>
<script>
const alertBox = document.getElementById('alert-box');
const btnSubmit = document.getElementById('btn-submit');
function showError(message) {
alertBox.classList.remove('hidden');
alertBox.textContent = message;
}
function hideError() {
alertBox.classList.add('hidden');
alertBox.textContent = '';
}
function getNumberValue(id) {
return Number(document.getElementById(id).value || 0);
}
function getAccessToken() {
return localStorage.getItem('access_token');
}
function getUserRole() {
return localStorage.getItem('user_role');
}
function redirectByRole(role) {
if (role === 'admin') {
window.location.href = "{{ route('admin.dashboard') }}";
return;
}
if (role === 'siswa') {
window.location.href = "{{ route('student.profile') }}";
return;
}
window.location.href = "{{ route('login') }}";
}
function validateForm(payload) {
const scoreFields = [
'agama', 'pkn', 'bahasa_indonesia', 'bahasa_inggris', 'matematika',
'fisika', 'kimia', 'biologi', 'sejarah', 'geografi',
'ekonomi_mapel', 'sosiologi', 'seni_budaya', 'pjok', 'informatika'
];
for (const field of scoreFields) {
const value = Number(payload[field]);
if (isNaN(value) || value < 0 || value > 100) {
return `Nilai ${field.replaceAll('_', ' ')} harus berada pada rentang 0 sampai 100.`;
}
}
if (Number(payload.gaji_ortu) < 0) {
return 'Gaji orang tua tidak boleh bernilai negatif.';
}
return null;
}
async function handleLogout() {
const token = getAccessToken();
if (token) {
try {
await fetch('/api/logout', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/json'
}
});
} catch (error) {
console.error('Logout error:', error);
}
}
localStorage.removeItem('access_token');
localStorage.removeItem('user_role');
window.location.href = "{{ route('login') }}";
}
document.addEventListener('DOMContentLoaded', function () {
const token = getAccessToken();
const role = getUserRole();
if (!token) {
window.location.href = "{{ route('login') }}";
return;
}
if (role !== 'siswa') {
redirectByRole(role);
}
});
btnSubmit.addEventListener('click', async function () {
hideError();
const token = getAccessToken();
const role = getUserRole();
if (!token) {
showError('Sesi login tidak ditemukan. Silakan login kembali.');
setTimeout(() => {
window.location.href = "{{ route('login') }}";
}, 1200);
return;
}
if (role !== 'siswa') {
redirectByRole(role);
return;
}
btnSubmit.innerHTML = 'Memproses Analisis...';
btnSubmit.disabled = true;
const scores = { r: 0, i: 0, a: 0, s: 0, e: 0, c: 0 };
document.querySelectorAll('.riasec-chk:checked').forEach(chk => {
scores[chk.dataset.type] += 50;
});
const payload = {
agama: getNumberValue('agama'),
pkn: getNumberValue('pkn'),
bahasa_indonesia: getNumberValue('bahasa_indonesia'),
bahasa_inggris: getNumberValue('bahasa_inggris'),
matematika: getNumberValue('matematika'),
fisika: getNumberValue('fisika'),
kimia: getNumberValue('kimia'),
biologi: getNumberValue('biologi'),
sejarah: getNumberValue('sejarah'),
geografi: getNumberValue('geografi'),
ekonomi_mapel: getNumberValue('ekonomi_mapel'),
sosiologi: getNumberValue('sosiologi'),
seni_budaya: getNumberValue('seni_budaya'),
pjok: getNumberValue('pjok'),
informatika: getNumberValue('informatika'),
riasec_r: scores.r,
riasec_i: scores.i,
riasec_a: scores.a,
riasec_s: scores.s,
riasec_e: scores.e,
riasec_c: scores.c,
gaji_ortu: getNumberValue('gaji_ortu')
};
const validationError = validateForm(payload);
if (validationError) {
showError(validationError);
btnSubmit.innerHTML = 'Jalankan AI Engine';
btnSubmit.disabled = false;
return;
}
try {
const response = await fetch('/api/student/proses-rekomendasi', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(payload)
});
const result = await response.json();
if (response.ok && result.success) {
window.location.href = result.redirect_url;
return;
}
if (response.status === 401) {
showError('Sesi login berakhir. Silakan login kembali.');
localStorage.removeItem('access_token');
localStorage.removeItem('user_role');
setTimeout(() => {
window.location.href = "{{ route('login') }}";
}, 1200);
return;
}
if (response.status === 403) {
showError(result.message || 'Anda tidak memiliki akses ke halaman ini.');
return;
}
showError(result.message || 'Terjadi kesalahan saat memproses rekomendasi.');
} catch (error) {
showError('Kesalahan jaringan. Pastikan Laravel dan API Flask sedang berjalan.');
} finally {
btnSubmit.innerHTML = 'Jalankan AI Engine';
btnSubmit.disabled = false;
}
});
</script>
</body>
</html>

40
routes/api.php Normal file
View File

@ -0,0 +1,40 @@
<?php
use App\Http\Controllers\Api\AuthController;
use App\Http\Controllers\Api\Admin\SiswaController;
use App\Http\Controllers\Api\Admin\JurusanController;
use App\Http\Controllers\Api\Admin\GuruController;
use App\Http\Controllers\Api\Admin\AlumniController;
use App\Http\Controllers\Api\Admin\DashboardAdminController;
use App\Http\Controllers\Api\Admin\RiwayatController;
use App\Http\Controllers\Api\Admin\ProspekKerjaController;
use App\Http\Controllers\Api\Student\RecommendationApiController;
use App\Http\Controllers\Api\Student\DashboardStudentController;
use Illuminate\Support\Facades\Route;
Route::post('/login', [AuthController::class, 'login']);
Route::middleware('auth:sanctum')->group(function () {
Route::middleware('role:admin')->prefix('admin')->group(function () {
Route::apiResource('siswa', SiswaController::class);
Route::apiResource('jurusan', JurusanController::class);
Route::apiResource('guru', GuruController::class);
Route::apiResource('alumni', AlumniController::class);
Route::apiResource('prospek-kerja', ProspekKerjaController::class);
Route::get('/dashboard-stats', [DashboardAdminController::class, 'index']);
Route::get('/riwayat', [RiwayatController::class, 'index']);
Route::get('/riwayat/{id}', [RiwayatController::class, 'show']);
});
Route::middleware('role:siswa')->prefix('student')->group(function () {
Route::get('/dashboard-stats', [DashboardStudentController::class, 'index']);
Route::post('/proses-rekomendasi', [RecommendationApiController::class, 'hitungPrediksi']);
Route::get('/hasil-rekomendasi/{id}', [RecommendationApiController::class, 'getHasilRekomendasi']);
});
Route::post('/logout', [AuthController::class, 'logout']);
});

View File

@ -1,7 +1,45 @@
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\LandingPageController;
use App\Http\Controllers\Student\StudentDashboardController;
use App\Http\Controllers\Student\RecommendationController;
use App\Http\Controllers\Admin\AdminDashboardController;
use App\Http\Controllers\Admin\SiswaController;
use App\Http\Controllers\Admin\JurusanController;
use App\Http\Controllers\Admin\GuruController;
use App\Http\Controllers\Admin\RiwayatController;
use App\Http\Controllers\Admin\AlumniController;
use App\Http\Controllers\Admin\ProspekKerjaController;
use App\Http\Controllers\AuthWebController;
use App\Http\Controllers\PanduanController;
Route::get('/', function () {
return view('welcome');
Route::get('/', [LandingPageController::class, 'index'])->name('landingpage');
Route::get('/login', function () { return view('auth.login'); })->name('login');
Route::get('/panduan', [PanduanController::class, 'showPanduan'])->name('panduan');
Route::prefix('student')->name('student.')->group(function () {
Route::get('/profile', [StudentDashboardController::class, 'profile'])->name('profile');
Route::get('/prediction', [RecommendationController::class, 'prediction'])->name('prediction');
Route::get('/result/{id}', [RecommendationController::class, 'result'])->name('result');
Route::get('/grades', [RecommendationController::class, 'grades'])->name('grades');
Route::get('/interest', [RecommendationController::class, 'interest'])->name('interest');
});
Route::prefix('admin')->name('admin.')->group(function () {
Route::get('/dashboard', [AdminDashboardController::class, 'index'])->name('dashboard');
Route::resource('siswa', SiswaController::class)->only(['index', 'create', 'edit'])->names(['index' => 'siswa']);
Route::resource('alumni', AlumniController::class)->only(['index', 'create', 'edit'])->names(['index' => 'alumni']);
Route::resource('jurusan', JurusanController::class)->only(['index', 'create', 'edit'])->names(['index' => 'jurusan']);
Route::resource('guru', GuruController::class)->only(['index', 'create', 'edit'])->names(['index' => 'guru']);
Route::resource('prospek-kerja', ProspekKerjaController::class)->only(['index', 'create', 'edit'])->names(['index' => 'prospek-kerja']);
Route::get('/riwayat', [RiwayatController::class, 'index'])->name('riwayat');
Route::get('/latih-model', function() {
return view('admin.latihmodel');
})->name('latih-model');
});