new update

This commit is contained in:
GalangSefin 2026-05-21 07:14:34 +07:00
parent 68b3232ef2
commit f1d9460598
36 changed files with 5747 additions and 15288 deletions

View File

@ -9,10 +9,36 @@
class AlumniController extends Controller
{
public function index()
public function index(Request $request)
{
$alumni = Alumni::all();
return response()->json(['success' => true, 'data' => $alumni], 200);
$perPage = (int) $request->input('per_page', 7);
$perPage = in_array($perPage, [5, 7]) ? $perPage : 7;
$search = trim((string) $request->input('search', ''));
$query = Alumni::query()->orderBy('id', 'desc');
if ($search !== '') {
$query->where(function ($q) use ($search) {
$q->where('id_alumni', 'like', "%{$search}%")
->orWhere('nama_alumni', 'like', "%{$search}%")
->orWhere('perguruan_tinggi', 'like', "%{$search}%")
->orWhere('jurusan_diambil', 'like', "%{$search}%");
});
}
$alumni = $query->paginate($perPage);
return response()->json([
'success' => true,
'data' => $alumni->items(),
'current_page' => $alumni->currentPage(),
'last_page' => $alumni->lastPage(),
'per_page' => $alumni->perPage(),
'total' => $alumni->total(),
'from' => $alumni->firstItem(),
'to' => $alumni->lastItem(),
], 200);
}
public function store(Request $request)
@ -32,9 +58,10 @@ public function store(Request $request)
}
$alumni = Alumni::create($request->all());
return response()->json([
'success' => true,
'message' => 'Alumni ' . $alumni->id_alumni . ' berhasil ditambahkan.',
'success' => true,
'message' => 'Alumni ' . $alumni->id_alumni . ' berhasil ditambahkan.',
'data' => $alumni
], 201);
}
@ -42,7 +69,10 @@ public function store(Request $request)
public function update(Request $request, $id)
{
$alumni = Alumni::find($id);
if (!$alumni) return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
if (!$alumni) {
return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
}
$validator = Validator::make($request->all(), [
'nama_alumni' => 'required|string|max:255',
@ -55,14 +85,23 @@ public function update(Request $request, $id)
}
$alumni->update($request->all());
return response()->json(['success' => true, 'message' => 'Data ' . $alumni->id_alumni . ' berhasil diperbarui'], 200);
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);
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

@ -14,34 +14,58 @@ 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)
$tableName = (new RiwayatRekomendasi())->getTable();
$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')
$totalRekomendasi = RiwayatRekomendasi::count();
$kolomAkurasi = null;
if (Schema::hasColumn($tableName, 'akurasi_prediksi')) {
$kolomAkurasi = 'akurasi_prediksi';
} elseif (Schema::hasColumn($tableName, 'probabilitas')) {
$kolomAkurasi = 'probabilitas';
} elseif (Schema::hasColumn($tableName, 'akurasi')) {
$kolomAkurasi = 'akurasi';
}
$rataAkurasi = null;
if ($kolomAkurasi) {
$avg = RiwayatRekomendasi::whereNotNull($kolomAkurasi)->avg($kolomAkurasi);
$rataAkurasi = $avg !== null ? round((float) $avg, 2) : null;
}
$akurasiStatus = $rataAkurasi !== null
? 'Berdasarkan rata-rata tingkat keyakinan dari seluruh hasil rekomendasi.'
: 'Belum ada nilai keyakinan prediksi yang tersimpan. Lakukan prediksi baru terlebih dahulu.';
$selectDistribusi = 'hasil_rekomendasi_jurusan as jurusan, COUNT(*) as total';
if ($kolomAkurasi) {
$selectDistribusi .= ', ROUND(AVG(' . $kolomAkurasi . '), 2) as rata_akurasi';
}
$distribusiJurusan = RiwayatRekomendasi::selectRaw($selectDistribusi)
->whereNotNull('hasil_rekomendasi_jurusan')
->groupBy('hasil_rekomendasi_jurusan')
->orderBy('total', 'desc')
->get();
->limit(10)
->get()
->map(function ($item) use ($kolomAkurasi) {
return [
'jurusan' => $item->jurusan,
'total' => (int) $item->total,
'rata_akurasi' => $kolomAkurasi && $item->rata_akurasi !== null
? round((float) $item->rata_akurasi, 2)
: null,
];
});
return response()->json([
'success' => true,
@ -49,7 +73,8 @@ public function index(Request $request)
'total_siswa' => $totalSiswa,
'rekomendasi_diproses' => $totalRekomendasi,
'rata_rata_akurasi' => $rataAkurasi,
'distribusi_jurusan' => $distribusiJurusan
'akurasi_status' => $akurasiStatus,
'distribusi_jurusan' => $distribusiJurusan,
]
], 200);

View File

@ -9,10 +9,34 @@
class GuruController extends Controller
{
public function index()
public function index(Request $request)
{
$guru = Guru::all();
return response()->json(['success' => true, 'data' => $guru], 200);
$perPage = (int) $request->input('per_page', 7);
$perPage = in_array($perPage, [5, 7]) ? $perPage : 7;
$search = trim((string) $request->input('search', ''));
$query = Guru::query()->orderBy('id', 'desc');
if ($search !== '') {
$query->where(function ($q) use ($search) {
$q->where('id_guru', 'like', "%{$search}%")
->orWhere('nama_guru', 'like', "%{$search}%");
});
}
$guru = $query->paginate($perPage);
return response()->json([
'success' => true,
'data' => $guru->items(),
'current_page' => $guru->currentPage(),
'last_page' => $guru->lastPage(),
'per_page' => $guru->perPage(),
'total' => $guru->total(),
'from' => $guru->firstItem(),
'to' => $guru->lastItem(),
], 200);
}
public function store(Request $request)
@ -28,9 +52,10 @@ public function store(Request $request)
}
$guru = Guru::create($request->all());
return response()->json([
'success' => true,
'message' => 'Guru ' . $guru->id_guru . ' berhasil ditambahkan.',
'success' => true,
'message' => 'Guru ' . $guru->id_guru . ' berhasil ditambahkan.',
'data' => $guru
], 201);
}
@ -38,7 +63,10 @@ public function store(Request $request)
public function update(Request $request, $id)
{
$guru = Guru::find($id);
if (!$guru) return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
if (!$guru) {
return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
}
$validator = Validator::make($request->all(), [
'nama_guru' => 'required|string|max:255',
@ -51,14 +79,23 @@ public function update(Request $request, $id)
}
$guru->update($request->all());
return response()->json(['success' => true, 'message' => 'Data ' . $guru->id_guru . ' berhasil diperbarui'], 200);
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);
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

@ -9,10 +9,35 @@
class JurusanController extends Controller
{
public function index()
public function index(Request $request)
{
$jurusan = Jurusan::all();
return response()->json(['success' => true, 'data' => $jurusan], 200);
$perPage = (int) $request->input('per_page', 7);
$perPage = in_array($perPage, [5, 7]) ? $perPage : 7;
$search = trim((string) $request->input('search', ''));
$query = Jurusan::query()->orderBy('id', 'desc');
if ($search !== '') {
$query->where(function ($q) use ($search) {
$q->where('nama_jurusan', 'like', "%{$search}%")
->orWhere('rumpun', 'like', "%{$search}%")
->orWhere('prospek_karir', 'like', "%{$search}%");
});
}
$jurusan = $query->paginate($perPage);
return response()->json([
'success' => true,
'data' => $jurusan->items(),
'current_page' => $jurusan->currentPage(),
'last_page' => $jurusan->lastPage(),
'per_page' => $jurusan->perPage(),
'total' => $jurusan->total(),
'from' => $jurusan->firstItem(),
'to' => $jurusan->lastItem(),
], 200);
}
public function store(Request $request)
@ -27,31 +52,52 @@ public function store(Request $request)
}
$jurusan = Jurusan::create($request->all());
return response()->json(['success' => true, 'message' => 'Jurusan berhasil ditambahkan', 'data' => $jurusan], 201);
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);
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);
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);
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);
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

@ -9,10 +9,38 @@
class ProspekKerjaController extends Controller
{
public function index()
public function index(Request $request)
{
$prospek = ProspekKerja::with('jurusan')->orderBy('id', 'desc')->get();
return response()->json(['success' => true, 'data' => $prospek], 200);
$perPage = (int) $request->input('per_page', 7);
$perPage = in_array($perPage, [5, 7]) ? $perPage : 7;
$search = trim((string) $request->input('search', ''));
$query = ProspekKerja::with('jurusan')->orderBy('id', 'desc');
if ($search !== '') {
$query->where(function ($q) use ($search) {
$q->where('nama_profesi', 'like', "%{$search}%")
->orWhere('range_gaji', 'like', "%{$search}%")
->orWhere('deskripsi_pekerjaan', 'like', "%{$search}%")
->orWhereHas('jurusan', function ($jurusan) use ($search) {
$jurusan->where('nama_jurusan', 'like', "%{$search}%");
});
});
}
$prospek = $query->paginate($perPage);
return response()->json([
'success' => true,
'data' => $prospek->items(),
'current_page' => $prospek->currentPage(),
'last_page' => $prospek->lastPage(),
'per_page' => $prospek->perPage(),
'total' => $prospek->total(),
'from' => $prospek->firstItem(),
'to' => $prospek->lastItem(),
], 200);
}
public function store(Request $request)
@ -29,20 +57,32 @@ public function store(Request $request)
}
$prospek = ProspekKerja::create($request->all());
return response()->json(['success' => true, 'message' => 'Prospek Kerja berhasil ditambahkan', 'data' => $prospek], 201);
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);
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);
if (!$prospek) {
return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
}
$validator = Validator::make($request->all(), [
'jurusan_id' => 'required|exists:jurusans,id',
@ -56,15 +96,24 @@ public function update(Request $request, $id)
}
$prospek->update($request->all());
return response()->json(['success' => true, 'message' => 'Prospek Kerja berhasil diperbarui', 'data' => $prospek], 200);
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);
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

@ -5,31 +5,57 @@
use App\Http\Controllers\Controller;
use App\Models\RiwayatRekomendasi;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Schema;
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'
];
$perPage = (int) $request->input('per_page', 7);
$perPage = in_array($perPage, [5, 7]) ? $perPage : 7;
$search = trim((string) $request->input('search', ''));
$query = RiwayatRekomendasi::with('user:id,name,email')
->orderBy('created_at', 'desc');
if ($search !== '') {
$query->where(function ($q) use ($search) {
$q->where('hasil_rekomendasi_jurusan', 'like', "%{$search}%")
->orWhereHas('user', function ($user) use ($search) {
$user->where('name', 'like', "%{$search}%")
->orWhere('email', 'like', "%{$search}%");
});
});
}
$riwayat = $query->paginate($perPage);
$riwayat->getCollection()->transform(function ($item) {
return [
'id' => $item->id,
'tanggal' => $item->created_at ? $item->created_at->format('d M Y') : '-',
'jam' => $item->created_at ? $item->created_at->format('H:i') : '-',
'nama_siswa' => $item->user ? $item->user->name : 'Siswa Tidak Diketahui',
'email_siswa' => $item->user ? $item->user->email : '-',
'hasil' => $item->hasil_rekomendasi_jurusan ?? 'Belum ada hasil',
'akurasi_prediksi' => $this->getAkurasiPrediksi($item),
'minat_utama' => $this->getMinatUtama($item),
'rata_nilai' => $this->getRataNilai($item),
];
});
return response()->json([
'success' => true,
'data' => $riwayat
'data' => $riwayat->items(),
'current_page' => $riwayat->currentPage(),
'last_page' => $riwayat->lastPage(),
'per_page' => $riwayat->perPage(),
'total' => $riwayat->total(),
'from' => $riwayat->firstItem(),
'to' => $riwayat->lastItem(),
], 200);
} catch (Exception $e) {
@ -40,9 +66,6 @@ public function index(Request $request)
}
}
/**
* Mengambil data detail riwayat prediksi berdasarkan ID
*/
public function show($id)
{
try {
@ -52,11 +75,33 @@ public function show($id)
'success' => true,
'data' => [
'id' => $detail->id,
'tanggal' => $detail->created_at->format('d M Y H:i:s'),
'tanggal' => $detail->created_at ? $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
'akurasi_prediksi' => $this->getAkurasiPrediksi($detail),
'minat_utama' => $this->getMinatUtama($detail),
'rata_nilai' => $this->getRataNilai($detail),
'kondisi_ekonomi' => $this->getLabelEkonomi($detail->gaji_ortu),
'gaji_ortu' => $detail->gaji_ortu,
'nilai_ringkas' => [
'Matematika' => $detail->matematika,
'B. Indonesia' => $detail->bahasa_indonesia,
'B. Inggris' => $detail->bahasa_inggris,
'Fisika' => $detail->fisika,
'Kimia' => $detail->kimia,
'Biologi' => $detail->biologi,
'Ekonomi' => $detail->ekonomi_mapel,
'Informatika' => $detail->informatika,
],
'riasec' => [
'Realistic' => $detail->riasec_r,
'Investigative' => $detail->riasec_i,
'Artistic' => $detail->riasec_a,
'Social' => $detail->riasec_s,
'Enterprising' => $detail->riasec_e,
'Conventional' => $detail->riasec_c,
],
]
], 200);
@ -67,4 +112,79 @@ public function show($id)
], 404);
}
}
private function getAkurasiPrediksi($riwayat): ?float
{
$tableName = (new RiwayatRekomendasi())->getTable();
if (!Schema::hasColumn($tableName, 'akurasi_prediksi')) {
return null;
}
if ($riwayat->akurasi_prediksi === null) {
return null;
}
return round((float) $riwayat->akurasi_prediksi, 2);
}
private function getRataNilai($riwayat): float
{
$nilai = [
$riwayat->agama,
$riwayat->pkn,
$riwayat->bahasa_indonesia,
$riwayat->bahasa_inggris,
$riwayat->matematika,
$riwayat->fisika,
$riwayat->kimia,
$riwayat->biologi,
$riwayat->sejarah,
$riwayat->geografi,
$riwayat->ekonomi_mapel,
$riwayat->sosiologi,
$riwayat->seni_budaya,
$riwayat->pjok,
$riwayat->informatika,
];
$nilai = array_filter($nilai, fn ($item) => $item !== null);
if (count($nilai) === 0) {
return 0;
}
return round(array_sum($nilai) / count($nilai), 2);
}
private function getMinatUtama($riwayat): string
{
$scores = [
'Teknis & Lapangan' => (float) $riwayat->riasec_r,
'Sains, Riset & Analitis' => (float) $riwayat->riasec_i,
'Kreatif & Desain' => (float) $riwayat->riasec_a,
'Sosial & Pelayanan' => (float) $riwayat->riasec_s,
'Bisnis & Kepemimpinan' => (float) $riwayat->riasec_e,
'Administratif & Terstruktur' => (float) $riwayat->riasec_c,
];
arsort($scores);
return array_key_first($scores) ?? 'Belum teridentifikasi';
}
private function getLabelEkonomi($gaji): string
{
$gaji = (float) $gaji;
if ($gaji < 2000000) {
return 'Kurang Mampu';
}
if ($gaji <= 5000000) {
return 'Menengah';
}
return 'Mampu';
}
}

View File

@ -9,10 +9,44 @@
class SiswaController extends Controller
{
public function index()
public function index(Request $request)
{
$siswa = Siswa::with(['user', 'jurusan'])->get();
return response()->json(['success' => true, 'data' => $siswa], 200);
$perPage = (int) $request->input('per_page', 7);
$perPage = in_array($perPage, [5, 7]) ? $perPage : 7;
$search = trim((string) $request->input('search', ''));
$query = Siswa::with(['user', 'jurusan'])->orderBy('id', 'desc');
if ($search !== '') {
$query->where(function ($q) use ($search) {
$q->where('id_siswa', 'like', "%{$search}%")
->orWhere('nama_lengkap', 'like', "%{$search}%")
->orWhere('nisn', 'like', "%{$search}%")
->orWhere('kelas', 'like', "%{$search}%")
->orWhere('peminatan', 'like', "%{$search}%")
->orWhereHas('user', function ($user) use ($search) {
$user->where('name', 'like', "%{$search}%")
->orWhere('email', 'like', "%{$search}%");
})
->orWhereHas('jurusan', function ($jurusan) use ($search) {
$jurusan->where('nama_jurusan', 'like', "%{$search}%");
});
});
}
$siswa = $query->paginate($perPage);
return response()->json([
'success' => true,
'data' => $siswa->items(),
'current_page' => $siswa->currentPage(),
'last_page' => $siswa->lastPage(),
'per_page' => $siswa->perPage(),
'total' => $siswa->total(),
'from' => $siswa->firstItem(),
'to' => $siswa->lastItem(),
], 200);
}
public function store(Request $request)
@ -25,7 +59,6 @@ public function store(Request $request)
'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.',
@ -35,16 +68,17 @@ public function store(Request $request)
if ($validator->fails()) {
return response()->json([
'success' => false,
'message' => 'Validasi gagal',
'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.',
'success' => true,
'message' => 'Siswa ' . $siswa->id_siswa . ' (' . $siswa->nama_lengkap . ') berhasil ditambahkan.',
'data' => $siswa
], 201);
}
@ -52,14 +86,21 @@ public function store(Request $request)
public function show($id)
{
$siswa = Siswa::find($id);
if (!$siswa) return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
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);
if (!$siswa) {
return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
}
$validator = Validator::make($request->all(), [
'nama_lengkap' => 'required|string|max:255',
@ -74,14 +115,23 @@ public function update(Request $request, $id)
}
$siswa->update($request->all());
return response()->json(['success' => true, 'message' => 'Data ' . $siswa->id_siswa . ' berhasil diperbarui'], 200);
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);
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

@ -7,6 +7,7 @@
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Schema;
class RecommendationApiController extends Controller
{
@ -48,10 +49,8 @@ public function hitungPrediksi(Request $request)
'gaji_ortu' => 'required|numeric|min:0',
]);
$ekonomiOrangTua = self::getLabelEkonomiSingkat($validated['gaji_ortu']);
$payload = $validated;
$payload['ekonomi_orang_tua'] = $ekonomiOrangTua;
$payload['ekonomi_orang_tua'] = self::getLabelEkonomiSingkat($validated['gaji_ortu']);
try {
$response = Http::timeout(15)
@ -83,8 +82,20 @@ public function hitungPrediksi(Request $request)
], 500);
}
$riwayat = RiwayatRekomendasi::create([
/*
* Ambil nilai probabilitas dari Flask.
* Contoh respons Flask yang disarankan:
* {
* "success": true,
* "prediksi_jurusan": "Teknik Informatika",
* "probabilitas": 86.45
* }
*/
$akurasiPrediksi = self::normalisasiAkurasi($responseData['probabilitas'] ?? null);
$dataRiwayat = [
'user_id' => $user->id,
'agama' => $validated['agama'],
'pkn' => $validated['pkn'],
'bahasa_indonesia' => $validated['bahasa_indonesia'],
@ -100,23 +111,38 @@ public function hitungPrediksi(Request $request)
'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,
]);
];
/*
* Kolom akurasi_prediksi hanya diisi kalau migration baru sudah dijalankan.
* Ini mencegah error kalau kolom belum ada.
*/
$tableName = (new RiwayatRekomendasi())->getTable();
if (Schema::hasColumn($tableName, 'akurasi_prediksi')) {
$dataRiwayat['akurasi_prediksi'] = $akurasiPrediksi;
}
$riwayat = RiwayatRekomendasi::create($dataRiwayat);
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
'akurasi_prediksi' => $akurasiPrediksi
]);
} catch (\Throwable $e) {
return response()->json([
'success' => false,
@ -140,6 +166,7 @@ public static function formatHasilData(RiwayatRekomendasi $riwayat): array
return [
'id' => $riwayat->id,
'hasil_rekomendasi_jurusan' => $riwayat->hasil_rekomendasi_jurusan,
'akurasi_prediksi' => $riwayat->akurasi_prediksi,
'label_ekonomi' => self::getLabelEkonomiDetail($riwayat->gaji_ortu),
'minat_utama' => self::getMinatUtamaLabel($riwayat),
'prospek_kerja' => self::getProspekKerjaText($riwayat->hasil_rekomendasi_jurusan),
@ -155,6 +182,43 @@ public static function formatHasilData(RiwayatRekomendasi $riwayat): array
];
}
private static function normalisasiAkurasi($value): ?float
{
if ($value === null || $value === '') {
return null;
}
if (is_string($value)) {
$value = str_replace('%', '', $value);
$value = str_replace(',', '.', $value);
$value = trim($value);
}
if (!is_numeric($value)) {
return null;
}
$number = (float) $value;
/*
* Jika Flask mengirim 0.86, ubah menjadi 86.
* Jika Flask mengirim 86.45, tetap 86.45.
*/
if ($number <= 1) {
$number = $number * 100;
}
if ($number < 0) {
$number = 0;
}
if ($number > 100) {
$number = 100;
}
return round($number, 2);
}
public static function getLabelEkonomiSingkat($gaji): string
{
$gaji = (float) $gaji;

View File

@ -10,7 +10,68 @@ class RiwayatRekomendasi extends Model
{
use HasFactory;
protected $guarded = ['id'];
protected $table = 'riwayat_rekomendasis';
protected $fillable = [
'user_id',
'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',
'gaji_ortu',
'hasil_rekomendasi_jurusan',
'akurasi_prediksi',
'detail_probabilitas',
];
protected $casts = [
'agama' => 'float',
'pkn' => 'float',
'bahasa_indonesia' => 'float',
'bahasa_inggris' => 'float',
'matematika' => 'float',
'fisika' => 'float',
'kimia' => 'float',
'biologi' => 'float',
'sejarah' => 'float',
'geografi' => 'float',
'ekonomi_mapel' => 'float',
'sosiologi' => 'float',
'seni_budaya' => 'float',
'pjok' => 'float',
'informatika' => 'float',
'riasec_r' => 'float',
'riasec_i' => 'float',
'riasec_a' => 'float',
'riasec_s' => 'float',
'riasec_e' => 'float',
'riasec_c' => 'float',
'gaji_ortu' => 'float',
'akurasi_prediksi' => 'float',
'detail_probabilitas' => 'array',
];
public function user(): BelongsTo
{

View File

@ -1,105 +0,0 @@
# %% [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

@ -1,80 +0,0 @@
# %% [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'!")
# %%

View File

@ -1,68 +0,0 @@
# %% [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

@ -0,0 +1,40 @@
<?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
{
if (!Schema::hasColumn('riwayat_rekomendasis', 'akurasi_prediksi')) {
Schema::table('riwayat_rekomendasis', function (Blueprint $table) {
$table->decimal('akurasi_prediksi', 5, 2)
->nullable()
->after('hasil_rekomendasi_jurusan');
});
}
if (!Schema::hasColumn('riwayat_rekomendasis', 'detail_probabilitas')) {
Schema::table('riwayat_rekomendasis', function (Blueprint $table) {
$table->json('detail_probabilitas')
->nullable()
->after('akurasi_prediksi');
});
}
}
public function down(): void
{
Schema::table('riwayat_rekomendasis', function (Blueprint $table) {
if (Schema::hasColumn('riwayat_rekomendasis', 'detail_probabilitas')) {
$table->dropColumn('detail_probabilitas');
}
if (Schema::hasColumn('riwayat_rekomendasis', 'akurasi_prediksi')) {
$table->dropColumn('akurasi_prediksi');
}
});
}
};

View File

@ -0,0 +1,7 @@
flask
flask-cors
pandas
numpy
scikit-learn==1.6.1
joblib
openpyxl

BIN
heatmap.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

View File

@ -3,12 +3,19 @@
================================ */
:root {
--primary-green: #2D7A47; /* Hijau SMAN 4 */
--primary-green: #2D7A47;
--dark-green: #1B4D2E;
--accent-yellow: #FFD700; /* Kuning Emas Logo */
--accent-yellow: #FFD700;
--bg-light: #f8fafc;
--white: #ffffff;
--sidebar-width: 260px;
--text-dark: #0f172a;
--text-muted: #64748b;
--border-soft: #e2e8f0;
}
* {
box-sizing: border-box;
}
body {
@ -16,6 +23,7 @@ body {
font-family: 'Figtree', sans-serif;
margin: 0;
overflow-x: hidden;
color: var(--text-dark);
}
/* Layout Container */
@ -24,7 +32,7 @@ .admin-wrapper {
min-height: 100vh;
}
/* --- PERBAIKAN SIDEBAR --- */
/* Sidebar */
.sidebar-modern {
width: var(--sidebar-width);
background-color: var(--primary-green);
@ -32,39 +40,46 @@ .sidebar-modern {
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);
height: 100vh;
box-shadow: 4px 0 18px rgba(15, 23, 42, 0.12);
z-index: 1000;
}
.sidebar-header {
padding: 30px 20px;
padding: 26px 20px;
text-align: center;
border-bottom: 1px solid rgba(255,255,255,0.1);
flex-shrink: 0; /* Header tidak akan mengecil saat di-scroll */
border-bottom: 1px solid rgba(255, 255, 255, 0.12);
flex-shrink: 0;
}
.sidebar-header img {
width: 60px;
width: 64px;
height: auto;
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;
.sidebar-header h5,
.sidebar-header h4,
.sidebar-header p {
color: var(--white);
margin-bottom: 0;
}
/* Area Menu Scroll */
.sidebar-menu {
padding: 18px 14px;
flex-grow: 1;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.28) 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);
background-color: rgba(255, 255, 255, 0.28);
border-radius: 10px;
}
@ -75,8 +90,8 @@ .sidebar-menu::-webkit-scrollbar-track {
.menu-label {
font-size: 11px;
text-transform: uppercase;
font-weight: 700;
color: rgba(255,255,255,0.5);
font-weight: 800;
color: rgba(255, 255, 255, 0.58);
margin: 20px 0 10px 10px;
letter-spacing: 1px;
}
@ -84,30 +99,37 @@ .menu-label {
.nav-item {
display: flex;
align-items: center;
padding: 12px 15px;
color: var(--white);
padding: 12px 14px;
color: rgba(255, 255, 255, 0.92);
text-decoration: none;
border-radius: 10px;
margin-bottom: 5px;
transition: all 0.3s;
font-weight: 500;
border-radius: 14px;
margin-bottom: 6px;
transition: all 0.25s ease;
font-weight: 600;
}
.nav-item i {
width: 25px;
font-size: 18px;
font-size: 17px;
margin-right: 10px;
text-align: center;
}
.nav-item:hover {
background-color: rgba(255,255,255,0.1);
background-color: rgba(255, 255, 255, 0.12);
color: var(--accent-yellow);
transform: translateX(3px);
}
.nav-item.active {
background-color: var(--white);
color: var(--primary-green);
font-weight: 700;
font-weight: 800;
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.12);
}
.nav-item.active i {
color: var(--primary-green);
}
/* Main Content Area */
@ -116,30 +138,36 @@ .main-content {
flex-grow: 1;
display: flex;
flex-direction: column;
min-width: 0; /* Mencegah overflow pada elemen flex */
min-width: 0;
}
/* Topbar */
.topbar {
height: 70px;
height: 72px;
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);
box-shadow: 0 4px 18px rgba(15, 23, 42, 0.05);
position: sticky;
top: 0;
z-index: 999;
border-bottom: 1px solid #eef2f7;
}
.search-bar {
background: #f1f5f9;
border-radius: 10px;
padding: 8px 15px;
border-radius: 14px;
padding: 9px 15px;
width: 350px;
display: flex;
align-items: center;
border: 1px solid #e2e8f0;
}
.search-bar i {
color: #94a3b8;
}
.search-bar input {
@ -148,58 +176,98 @@ .search-bar input {
margin-left: 10px;
outline: none;
width: 100%;
font-size: 14px;
color: var(--text-dark);
}
.search-bar input::placeholder {
color: #94a3b8;
}
/* Area Isi Halaman */
.content-wrapper,
.admin-content {
padding: 30px;
}
/* Stats Cards */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 25px;
gap: 24px;
padding: 30px;
}
.stat-card {
background: var(--white);
padding: 25px;
border-radius: 20px;
box-shadow: 0 4px 6px rgba(0,0,0,0.02);
border-radius: 22px;
box-shadow: 0 14px 35px rgba(15, 23, 42, 0.06);
display: flex;
align-items: center;
justify-content: space-between;
border: 1px solid #f1f5f9;
border: 1px solid #eef2f7;
transition: 0.25s ease;
}
.stat-card:hover {
transform: translateY(-3px);
box-shadow: 0 18px 42px rgba(15, 23, 42, 0.08);
}
.stat-info h3 {
margin: 0;
font-size: 28px;
font-weight: 800;
font-size: 30px;
font-weight: 900;
color: #1e293b;
letter-spacing: -0.5px;
}
.stat-info p {
margin: 0;
margin: 0 0 6px;
color: #64748b;
font-size: 14px;
font-weight: 600;
font-weight: 700;
}
.stat-icon {
width: 55px;
height: 55px;
border-radius: 15px;
width: 56px;
height: 56px;
border-radius: 18px;
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; }
.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;
margin: 30px;
padding: 28px;
background: var(--white);
border-radius: 24px;
border: 1px solid #eef2f7;
box-shadow: 0 16px 40px rgba(15, 23, 42, 0.06);
}
/* Jika dashboard-section berada setelah stats-grid, jaraknya lebih rapi */
.stats-grid + .dashboard-section {
margin-top: 0;
}
.chart-container {
@ -208,4 +276,63 @@ .chart-container {
padding: 25px;
border: 1px solid #f1f5f9;
min-height: 400px;
}
/* Responsive */
@media (max-width: 992px) {
:root {
--sidebar-width: 240px;
}
.topbar {
padding: 0 20px;
}
.search-bar {
width: 260px;
}
.stats-grid {
padding: 24px;
gap: 18px;
}
.dashboard-section {
margin: 24px;
padding: 22px;
}
}
@media (max-width: 768px) {
.sidebar-modern {
width: 230px;
transform: translateX(-100%);
}
.main-content {
margin-left: 0;
}
.topbar {
height: auto;
min-height: 68px;
gap: 14px;
flex-wrap: wrap;
padding: 14px 18px;
}
.search-bar {
width: 100%;
}
.stats-grid {
grid-template-columns: 1fr;
padding: 18px;
}
.dashboard-section {
margin: 18px;
padding: 18px;
border-radius: 18px;
}
}

View File

@ -0,0 +1,320 @@
/* ================================
DASHBOARD ADMIN - MODERN STYLE
================================ */
.dashboard-modern {
padding: 30px;
}
.dashboard-hero {
position: relative;
overflow: hidden;
border-radius: 30px;
padding: 28px;
background:
radial-gradient(circle at 10% 10%, rgba(255, 215, 0, 0.18), transparent 28%),
radial-gradient(circle at 90% 20%, rgba(34, 197, 94, 0.16), transparent 30%),
linear-gradient(135deg, #0f3d25 0%, #2D7A47 55%, #3fa263 100%);
color: #ffffff;
box-shadow: 0 24px 55px rgba(45, 122, 71, 0.22);
margin-bottom: 26px;
}
.dashboard-hero::before {
content: "";
position: absolute;
width: 260px;
height: 260px;
right: -70px;
bottom: -95px;
background: rgba(255, 255, 255, 0.13);
border-radius: 999px;
filter: blur(2px);
}
.dashboard-hero-content {
position: relative;
z-index: 2;
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
flex-wrap: wrap;
}
.hero-label {
display: inline-flex;
align-items: center;
gap: 8px;
background: rgba(255, 255, 255, 0.14);
border: 1px solid rgba(255, 255, 255, 0.16);
padding: 8px 12px;
border-radius: 999px;
font-size: 12px;
font-weight: 800;
letter-spacing: 0.4px;
margin-bottom: 14px;
}
.hero-title {
margin: 0;
font-size: 30px;
font-weight: 900;
letter-spacing: -0.7px;
}
.hero-subtitle {
margin: 10px 0 0;
max-width: 760px;
color: rgba(255, 255, 255, 0.82);
line-height: 1.7;
font-size: 14px;
}
.hero-date-card {
min-width: 210px;
background: rgba(255, 255, 255, 0.14);
border: 1px solid rgba(255, 255, 255, 0.16);
border-radius: 22px;
padding: 18px;
backdrop-filter: blur(12px);
}
.hero-date-card span {
display: block;
font-size: 12px;
color: rgba(255, 255, 255, 0.72);
font-weight: 700;
margin-bottom: 6px;
}
.hero-date-card strong {
font-size: 18px;
font-weight: 900;
}
.modern-stats-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 22px;
margin-bottom: 26px;
}
.modern-stat-card {
position: relative;
overflow: hidden;
background: #ffffff;
border: 1px solid #e8eef5;
border-radius: 26px;
padding: 24px;
box-shadow: 0 18px 46px rgba(15, 23, 42, 0.06);
transition: 0.25s ease;
}
.modern-stat-card:hover {
transform: translateY(-4px);
box-shadow: 0 24px 60px rgba(15, 23, 42, 0.09);
}
.modern-stat-card::after {
content: "";
position: absolute;
width: 120px;
height: 120px;
right: -48px;
top: -48px;
background: #ecfdf5;
border-radius: 999px;
}
.stat-card-content {
position: relative;
z-index: 2;
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
}
.stat-label {
margin: 0 0 8px;
color: #64748b;
font-size: 13px;
font-weight: 800;
}
.stat-value {
margin: 0;
color: #0f172a;
font-size: 34px;
font-weight: 950;
letter-spacing: -0.8px;
}
.stat-note {
margin: 8px 0 0;
color: #94a3b8;
font-size: 12px;
font-weight: 700;
line-height: 1.5;
}
.modern-stat-icon {
width: 58px;
height: 58px;
border-radius: 20px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 24px;
flex: 0 0 58px;
}
.icon-blue {
background: #eef2ff;
color: #4f46e5;
}
.icon-green {
background: #dcfce7;
color: #15803d;
}
.accuracy-ring {
width: 62px;
height: 62px;
border-radius: 999px;
display: inline-flex;
align-items: center;
justify-content: center;
background: conic-gradient(#2D7A47 0deg, #e2e8f0 0deg);
padding: 5px;
flex: 0 0 62px;
}
.accuracy-ring-inner {
width: 100%;
height: 100%;
border-radius: 999px;
background: #ffffff;
display: inline-flex;
align-items: center;
justify-content: center;
color: #2D7A47;
font-weight: 900;
font-size: 12px;
}
.dashboard-card {
background: #ffffff;
border: 1px solid #e8eef5;
border-radius: 28px;
padding: 24px;
box-shadow: 0 18px 46px rgba(15, 23, 42, 0.06);
}
.dashboard-chart-full {
width: 100%;
}
.card-header-modern {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
margin-bottom: 20px;
}
.card-title-modern {
margin: 0;
color: #0f172a;
font-weight: 900;
font-size: 18px;
letter-spacing: -0.3px;
}
.card-subtitle-modern {
margin: 6px 0 0;
color: #64748b;
font-size: 13px;
line-height: 1.6;
}
.chart-badge {
background: #ecfdf5;
color: #15803d;
border: 1px solid #bbf7d0;
border-radius: 999px;
padding: 8px 12px;
font-size: 12px;
font-weight: 900;
white-space: nowrap;
}
.chart-wrapper-modern {
height: 390px;
border-radius: 22px;
border: 1px solid #edf2f7;
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
padding: 18px;
position: relative;
}
.dashboard-placeholder {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
}
.dashboard-placeholder i {
font-size: 38px;
color: #cbd5e1;
margin-bottom: 12px;
}
.dashboard-placeholder p {
color: #64748b;
font-weight: 700;
margin: 0;
}
@media (max-width: 1100px) {
.modern-stats-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 768px) {
.dashboard-modern {
padding: 18px;
}
.dashboard-hero {
border-radius: 24px;
padding: 22px;
}
.hero-title {
font-size: 24px;
}
.modern-stat-card,
.dashboard-card {
border-radius: 22px;
padding: 20px;
}
.chart-wrapper-modern {
height: 320px;
}
.card-header-modern {
flex-direction: column;
}
.hero-date-card {
width: 100%;
}
}

View File

@ -1,155 +1,612 @@
/* ================================
FITUR ADMIN - TABLE, FORM, SEARCH, PAGINATION
================================ */
:root {
--primary-green: #2D7A47;
--primary-green-dark: #1B4D2E;
--soft-green: #ecfdf5;
--accent-yellow: #FFD700;
--danger-red: #ef4444;
--danger-red-dark: #dc2626;
--warning-yellow: #f59e0b;
--text-dark: #0f172a;
--text-muted: #64748b;
--border-soft: #e2e8f0;
--border-table: #d5dee8;
--bg-soft: #f8fafc;
--white: #ffffff;
}
/* Header halaman master */
.master-page-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 24px;
}
.master-page-title {
margin: 0;
font-weight: 900;
color: var(--text-dark);
letter-spacing: -0.4px;
}
.master-page-subtitle {
margin: 6px 0 0;
color: var(--text-muted);
font-size: 14px;
}
/* Tombol tambah data */
.btn-add-modern {
border: none;
background: linear-gradient(135deg, var(--primary-green), var(--primary-green-dark));
color: #ffffff !important;
padding: 12px 18px;
border-radius: 14px;
font-weight: 800;
text-decoration: none;
box-shadow: 0 12px 24px rgba(45, 122, 71, 0.22);
transition: 0.25s ease;
display: inline-flex;
align-items: center;
gap: 8px;
white-space: nowrap;
}
.btn-add-modern:hover {
transform: translateY(-2px);
box-shadow: 0 16px 30px rgba(45, 122, 71, 0.28);
color: #ffffff !important;
}
/* Container tabel */
.table-container-modern {
border: 1px solid #e2e8f0;
border-radius: 12px;
border: 1px solid var(--border-soft);
border-radius: 22px;
overflow: hidden;
background-color: #ffffff;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
box-shadow: 0 16px 36px rgba(15, 23, 42, 0.06);
}
/* Toolbar search dan jumlah data */
.table-toolbar {
padding: 20px;
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
border-bottom: 1px solid var(--border-soft);
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
flex-wrap: wrap;
}
.search-wrapper-modern {
position: relative;
flex: 1;
min-width: 260px;
}
.search-wrapper-modern i {
position: absolute;
left: 16px;
top: 50%;
transform: translateY(-50%);
color: #94a3b8;
font-size: 14px;
}
.search-input-modern {
width: 100%;
height: 46px;
border: 1px solid var(--border-soft);
border-radius: 14px;
padding: 0 16px 0 42px;
outline: none;
font-size: 14px;
color: var(--text-dark);
background: #ffffff;
transition: 0.25s ease;
}
.search-input-modern::placeholder {
color: #94a3b8;
}
.search-input-modern:focus {
border-color: var(--primary-green);
box-shadow: 0 0 0 4px rgba(45, 122, 71, 0.12);
}
.per-page-wrapper {
display: flex;
align-items: center;
gap: 10px;
color: var(--text-muted);
font-size: 14px;
font-weight: 700;
}
.per-page-select {
height: 46px;
border: 1px solid var(--border-soft);
border-radius: 14px;
padding: 0 12px;
outline: none;
color: var(--text-dark);
font-weight: 800;
background: #ffffff;
cursor: pointer;
}
.per-page-select:focus {
border-color: var(--primary-green);
box-shadow: 0 0 0 4px rgba(45, 122, 71, 0.12);
}
/* Tabel modern */
.table-responsive {
width: 100%;
overflow-x: auto;
}
.table-modern {
width: 100%;
border-collapse: collapse;
border-collapse: separate;
border-spacing: 0;
margin-bottom: 0 !important;
background: #ffffff;
}
/* Header tabel */
.table-modern thead th {
background-color: #f8fafc;
color: #64748b;
font-size: 11px;
color: #334155;
font-size: 12px;
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 */
letter-spacing: 0.7px;
font-weight: 900;
padding: 16px 18px;
border-bottom: 1px solid var(--border-table) !important;
white-space: nowrap;
}
.table-modern thead th:last-child {
border-right: none;
/* Garis pemisah antar kolom bagian header */
.table-modern thead th:not(:last-child) {
border-right: 1px solid var(--border-table) !important;
}
/* Isi tabel */
.table-modern tbody td {
padding: 15px 20px;
border-bottom: 1px solid #f1f5f9;
padding: 17px 18px;
border-bottom: 1px solid #e8eef5 !important;
vertical-align: middle;
border-right: 1px solid #f1f5f9; /* Garis antar kolom */
color: var(--text-dark);
font-size: 14px;
}
/* Garis pemisah antar kolom bagian isi tabel */
.table-modern tbody td:not(:last-child) {
border-right: 1px solid var(--border-table) !important;
}
/* Baris terakhir tidak perlu garis bawah */
.table-modern tbody tr:last-child td {
border-bottom: none !important;
}
.table-modern tbody tr {
transition: 0.22s ease;
}
.table-modern tbody tr:hover {
background-color: #f0fdf4;
}
/* Kolom nomor dan opsi lebih rapi */
.table-modern th.text-center,
.table-modern td.text-center {
text-align: center;
}
/* Kolom opsi dibuat lebih lebar agar teks tombol tidak sempit */
.table-modern thead th:last-child,
.table-modern tbody td:last-child {
border-right: none;
min-width: 190px;
}
/* 2. Opsi Tombol Aksi (Edit & Hapus) */
/* Cell user / nama */
.table-user-cell {
display: flex;
align-items: center;
gap: 12px;
}
.avatar-circle {
width: 40px;
height: 40px;
border-radius: 14px;
display: inline-flex;
align-items: center;
justify-content: center;
background: #ecfdf5;
color: var(--primary-green-dark);
font-weight: 900;
flex: 0 0 40px;
font-size: 15px;
}
.avatar-circle.blue {
background: #eff6ff;
color: #2563eb;
}
.avatar-circle.purple {
background: #faf5ff;
color: #9333ea;
}
.avatar-circle.orange {
background: #fff7ed;
color: #ea580c;
}
.text-cell-muted {
color: var(--text-muted);
font-size: 13px;
line-height: 1.6;
}
/* Badge */
.badge-soft {
border-radius: 999px;
padding: 8px 13px;
font-size: 12px;
font-weight: 900;
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid transparent;
line-height: 1;
white-space: nowrap;
}
.badge-kelas {
background: #ecfdf5;
color: #15803d;
border: 1px solid #bbf7d0;
}
.badge-saintek {
background-color: #eff6ff;
color: #1d4ed8;
border: 1px solid #bfdbfe;
}
.badge-soshum {
background-color: #fef3c7;
color: #b45309;
border: 1px solid #fde68a;
}
.badge-bk {
background-color: #ecfdf5;
color: #15803d;
border: 1px solid #bbf7d0;
}
.badge-result {
background: #dcfce7;
color: #166534;
border: 1px solid #bbf7d0;
}
/* Tetap mendukung class badge Bootstrap / lama */
.badge {
display: inline-block;
padding: 0.45em 0.75em;
font-size: 0.75em;
font-weight: 800;
line-height: 1;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
border-radius: 50rem;
}
/* Opsi tombol aksi */
.opsi-wrapper {
display: flex;
gap: 8px;
justify-content: center;
align-items: center;
flex-wrap: nowrap;
}
/* Tombol edit dan hapus dibuat jelas dengan teks */
.btn-action {
width: 32px;
height: 32px;
display: flex;
min-width: 84px;
height: 38px;
padding: 0 13px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 6px;
gap: 7px;
border-radius: 12px;
border: none;
color: #ffffff;
transition: transform 0.2s, opacity 0.2s;
color: #ffffff !important;
transition: 0.22s ease;
cursor: pointer;
text-decoration: none !important;
font-size: 13px;
font-weight: 900;
line-height: 1;
}
.btn-action i {
font-size: 13px;
}
.btn-action:hover {
transform: scale(1.1);
opacity: 0.9;
transform: translateY(-2px);
color: #ffffff !important;
text-decoration: none !important;
}
.btn-edit { background-color: #4f46e5; }
.btn-delete { background-color: #ef4444; }
.btn-edit {
background-color: #22c55e;
}
/* 3. Pagination (Menyatu di bawah tabel) */
.btn-edit:hover {
background-color: #16a34a;
box-shadow: 0 10px 18px rgba(34, 197, 94, 0.22);
}
.btn-delete {
background-color: #ef4444;
}
.btn-delete:hover {
background-color: #dc2626;
box-shadow: 0 10px 18px rgba(239, 68, 68, 0.22);
}
/* Menambahkan tulisan otomatis pada tombol yang di Blade masih hanya ikon */
.btn-edit::after {
content: "Edit";
}
.btn-delete::after {
content: "Hapus";
}
/* Tombol detail riwayat */
.btn-detail {
border: none;
background: #eff6ff;
color: #2563eb;
border-radius: 999px;
padding: 9px 14px;
font-weight: 900;
transition: 0.22s ease;
}
.btn-detail:hover {
background: #2563eb;
color: #ffffff;
}
/* Pagination */
.pagination-container {
padding: 15px 20px;
padding: 18px 20px;
display: flex;
justify-content: space-between;
align-items: center;
background-color: #ffffff;
border-top: 1px solid #e2e8f0;
border-top: 1px solid var(--border-soft);
gap: 14px;
flex-wrap: wrap;
}
.page-link-custom {
padding: 6px 14px;
border: 1px solid #e2e8f0;
min-width: 38px;
height: 38px;
padding: 0 12px;
border: 1px solid var(--border-soft);
background-color: #ffffff;
border-radius: 6px;
color: #64748b;
border-radius: 12px;
color: #475569;
font-size: 13px;
font-weight: 900;
cursor: pointer;
transition: all 0.2s;
transition: 0.22s ease;
}
.page-link-custom:hover:not(:disabled) {
background-color: #ecfdf5;
color: var(--primary-green);
border-color: #bbf7d0;
}
.page-link-custom.active {
background-color: #007bff;
background-color: var(--primary-green);
color: #ffffff;
border-color: #007bff;
border-color: var(--primary-green);
}
.page-link-custom:disabled {
background-color: #f8fafc;
cursor: not-allowed;
opacity: 0.5;
opacity: 0.55;
}
/* 4. Form Styling (Satu Baris Satu Input) */
/* Loading dan data kosong */
.empty-state-modern {
padding: 36px 20px !important;
text-align: center;
color: var(--text-muted) !important;
}
.empty-state-modern i {
display: block;
font-size: 28px;
margin-bottom: 10px;
color: #cbd5e1;
}
.loading-state-modern {
padding: 36px 20px !important;
text-align: center;
color: var(--text-muted) !important;
}
.loading-state-modern i {
color: var(--primary-green);
}
/* Form Styling */
.form-group-custom {
margin-bottom: 1.5rem;
}
.form-group-custom label {
font-size: 13px;
font-weight: 700;
font-weight: 800;
color: #475569;
margin-bottom: 8px;
display: block;
}
.input-custom {
border-radius: 10px !important;
border-radius: 12px !important;
border: 1px solid #e2e8f0 !important;
padding: 12px 15px !important;
font-size: 14px;
width: 100%;
background: #ffffff;
color: var(--text-dark);
}
.input-custom:focus {
border-color: var(--primary-green) !important;
box-shadow: 0 0 0 3px rgba(45, 122, 71, 0.1) !important;
box-shadow: 0 0 0 4px rgba(45, 122, 71, 0.12) !important;
outline: none;
}
/* Badge Rumpun Jurusan */
.badge-saintek {
background-color: #e0f2fe; /* Warna Biru Muda */
color: #0369a1; /* Warna Biru Tua */
border: 1px solid #bae6fd;
/* Card umum untuk form */
.form-card-modern {
background: #ffffff;
border-radius: 22px;
border: 1px solid var(--border-soft);
box-shadow: 0 16px 36px rgba(15, 23, 42, 0.06);
padding: 24px;
}
.badge-soshum {
background-color: #fef3c7; /* Warna Kuning Muda */
color: #92400e; /* Warna Cokelat/Oren */
border: 1px solid #fde68a;
/* Button umum form */
.btn-save-modern {
border: none;
background: linear-gradient(135deg, var(--primary-green), var(--primary-green-dark));
color: #ffffff;
padding: 11px 18px;
border-radius: 12px;
font-weight: 900;
transition: 0.22s ease;
}
/* 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;
.btn-save-modern:hover {
transform: translateY(-2px);
box-shadow: 0 12px 24px rgba(45, 122, 71, 0.22);
color: #ffffff;
}
.btn-back-modern {
border: 1px solid var(--border-soft);
background: #ffffff;
color: #475569;
padding: 11px 18px;
border-radius: 12px;
font-weight: 900;
text-decoration: none;
transition: 0.22s ease;
}
.btn-back-modern:hover {
background: #f8fafc;
color: var(--primary-green);
}
/* Responsive */
@media (max-width: 992px) {
.btn-action {
min-width: 78px;
padding: 0 11px;
font-size: 12px;
}
.table-modern thead th:last-child,
.table-modern tbody td:last-child {
min-width: 175px;
}
}
@media (max-width: 768px) {
.master-page-header {
align-items: flex-start;
flex-direction: column;
}
.btn-add-modern {
width: 100%;
justify-content: center;
}
.table-toolbar {
align-items: stretch;
flex-direction: column;
}
.search-wrapper-modern {
min-width: 100%;
}
.per-page-wrapper {
justify-content: space-between;
}
.per-page-select {
min-width: 120px;
}
.pagination-container {
align-items: flex-start;
flex-direction: column;
}
.table-modern thead th,
.table-modern tbody td {
padding: 14px 14px;
font-size: 13px;
}
.avatar-circle {
width: 36px;
height: 36px;
border-radius: 12px;
flex-basis: 36px;
}
.opsi-wrapper {
flex-direction: column;
gap: 6px;
}
.btn-action {
min-width: 78px;
height: 34px;
font-size: 12px;
border-radius: 10px;
}
}

View File

@ -0,0 +1,211 @@
/* ================================
RIWAYAT ADMIN - KHUSUS HALAMAN RIWAYAT
================================ */
.riwayat-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 18px;
margin-bottom: 24px;
}
.riwayat-label {
display: inline-flex;
align-items: center;
gap: 8px;
background: #ecfdf5;
color: #15803d;
border: 1px solid #bbf7d0;
border-radius: 999px;
padding: 8px 13px;
font-size: 12px;
font-weight: 900;
margin-bottom: 12px;
}
.riwayat-date-main {
color: #0f172a;
font-size: 13px;
font-weight: 900;
}
.riwayat-date-sub {
color: #64748b;
font-size: 12px;
font-weight: 700;
margin-top: 4px;
}
.riwayat-user-cell {
display: flex;
align-items: center;
gap: 12px;
}
.riwayat-avatar {
width: 42px;
height: 42px;
border-radius: 16px;
background: #ecfdf5;
color: #166534;
display: flex;
align-items: center;
justify-content: center;
font-weight: 900;
flex: 0 0 42px;
}
.riwayat-user-name {
color: #0f172a;
font-size: 14px;
font-weight: 900;
margin-bottom: 3px;
}
.riwayat-user-email {
color: #64748b;
font-size: 12px;
font-weight: 700;
}
.riwayat-input-summary {
display: flex;
flex-direction: column;
gap: 5px;
color: #64748b;
font-size: 12px;
font-weight: 700;
line-height: 1.5;
}
.confidence-badge {
display: inline-flex;
align-items: center;
gap: 6px;
border-radius: 999px;
padding: 8px 12px;
background: #eff6ff;
color: #2563eb;
border: 1px solid #bfdbfe;
font-size: 12px;
font-weight: 900;
white-space: nowrap;
}
.detail-result-card {
border-radius: 22px;
padding: 18px;
background:
radial-gradient(circle at 10% 10%, rgba(255, 215, 0, 0.14), transparent 25%),
linear-gradient(135deg, #ecfdf5 0%, #ffffff 100%);
border: 1px solid #bbf7d0;
margin-bottom: 16px;
}
.detail-result-card small {
display: block;
color: #15803d;
font-size: 11px;
font-weight: 900;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 8px;
}
.detail-result-card h4 {
margin: 0;
color: #0f172a;
font-size: 22px;
font-weight: 950;
}
.detail-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
.detail-info-card {
background: #f8fafc;
border: 1px solid #e8eef5;
border-radius: 18px;
padding: 15px;
}
.detail-info-card span {
display: block;
color: #64748b;
font-size: 11px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.4px;
margin-bottom: 7px;
}
.detail-info-card strong {
display: block;
color: #0f172a;
font-size: 15px;
font-weight: 900;
line-height: 1.5;
}
.detail-section-title {
margin: 18px 0 12px;
color: #0f172a;
font-size: 15px;
font-weight: 950;
}
.mini-data-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
}
.mini-data-item {
border-radius: 14px;
background: #f8fafc;
border: 1px solid #e8eef5;
padding: 11px;
}
.mini-data-item span {
display: block;
color: #64748b;
font-size: 11px;
font-weight: 800;
margin-bottom: 4px;
}
.mini-data-item strong {
color: #0f172a;
font-size: 14px;
font-weight: 950;
}
.modal-content {
border-radius: 24px !important;
}
@media (max-width: 992px) {
.mini-data-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 768px) {
.riwayat-header {
flex-direction: column;
}
.detail-grid,
.mini-data-grid {
grid-template-columns: 1fr;
}
.riwayat-user-cell {
align-items: flex-start;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View File

@ -5,22 +5,40 @@
<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 class="master-page-header">
<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>
<h4 class="master-page-title">Manajemen Data Alumni</h4>
<p class="master-page-subtitle">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 href="{{ route('admin.alumni.create') }}" class="btn-add-modern">
<i class="fas fa-plus"></i>
Tambah Data Alumni
</a>
</div>
<div class="table-container-modern shadow-sm">
<div class="table-toolbar">
<div class="search-wrapper-modern">
<i class="fas fa-search"></i>
<input type="text" id="searchInput" class="search-input-modern" placeholder="Cari nama alumni, perguruan tinggi, atau jurusan...">
</div>
<div class="per-page-wrapper">
<span>Tampilkan</span>
<select id="perPageSelect" class="per-page-select">
<option value="7" selected>7 data</option>
<option value="5">5 data</option>
</select>
</div>
</div>
<div class="table-responsive">
<table class="table table-modern mb-0">
<thead>
<tr>
<th class="text-center" width="100">ID Alumni</th>
<th class="text-center" width="70">No</th>
<th class="text-center" width="120">ID Alumni</th>
<th>Nama Alumni</th>
<th>Perguruan Tinggi</th>
<th class="text-center">Jurusan Diambil</th>
@ -28,47 +46,125 @@
</tr>
</thead>
<tbody id="alumni-table-body">
<tr><td colspan="5" class="text-center py-5 text-muted">Memuat data alumni...</td></tr>
<tr>
<td colspan="6" class="loading-state-modern">
<i class="fas fa-spinner fa-spin me-2"></i> 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 class="d-flex gap-1 flex-wrap" id="pagination-controls"></div>
</div>
</div>
</div>
<script>
let currentPage = 1;
let currentSearch = '';
let perPage = 7;
let searchTimer = null;
function escapeHtml(value) {
if (value === null || value === undefined || value === '') return '-';
return String(value)
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#039;');
}
function getInitial(name) {
const cleanName = name || 'A';
return cleanName.charAt(0).toUpperCase();
}
function showLoading() {
document.getElementById('alumni-table-body').innerHTML = `
<tr>
<td colspan="6" class="loading-state-modern">
<i class="fas fa-spinner fa-spin me-2"></i> Memuat data alumni...
</td>
</tr>
`;
}
async function fetchAlumni(page = 1) {
const token = localStorage.getItem('access_token');
const tbody = document.getElementById('alumni-table-body');
if (!token) {
window.location.href = '/login';
return;
}
currentPage = page;
showLoading();
const params = new URLSearchParams({
page: page,
per_page: perPage
});
if (currentSearch !== '') {
params.append('search', currentSearch);
}
try {
const response = await fetch(`/api/admin/alumni?page=${page}`, {
headers: {
const response = await fetch(`/api/admin/alumni?${params.toString()}`, {
headers: {
'Authorization': 'Bearer ' + token,
'Accept': 'application/json'
}
});
const result = await response.json();
const tbody = document.getElementById('alumni-table-body');
if (!response.ok || result.success === false) {
throw new Error(result.message || 'Gagal memuat data alumni.');
}
const data = result.data || [];
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>';
if (data.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="6" class="empty-state-modern">
<i class="fas fa-folder-open"></i>
Tidak ada data alumni yang ditemukan.
</td>
</tr>
`;
updatePaginationInfo(null);
renderPagination({ current_page: 1, last_page: 1, total: 0 });
return;
}
result.data.forEach((item) => {
data.forEach((item, index) => {
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 text-muted fw-bold">${(result.from || 1) + index}</td>
<td class="text-center fw-bold text-muted">${escapeHtml(item.id_alumni)}</td>
<td>
<div class="table-user-cell">
<div class="avatar-circle blue">${getInitial(item.nama_alumni)}</div>
<div>
<div class="fw-bold text-dark">${escapeHtml(item.nama_alumni)}</div>
<div class="text-cell-muted">Data alumni</div>
</div>
</div>
</td>
<td class="text-cell-muted">${escapeHtml(item.perguruan_tinggi)}</td>
<td class="text-center">
<span class="badge-soft badge-result">${escapeHtml(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">
@ -79,66 +175,134 @@
</button>
</div>
</td>
</tr>`;
</tr>
`;
});
document.getElementById('pagination-info').innerText =
`Menampilkan ${result.from || 0} sampai ${result.to || 0} dari ${result.total || 0} data`;
updatePaginationInfo(result);
renderPagination(result);
} catch (error) {
tbody.innerHTML = '<tr><td colspan="5" class="text-center text-danger">Gagal memuat data.</td></tr>';
} catch (error) {
tbody.innerHTML = `
<tr>
<td colspan="6" class="empty-state-modern text-danger">
<i class="fas fa-exclamation-circle"></i>
Gagal memuat data alumni.
</td>
</tr>
`;
}
}
function updatePaginationInfo(data) {
const info = document.getElementById('pagination-info');
if (!data || !data.total) {
info.innerText = 'Menampilkan 0 sampai 0 dari 0 data';
return;
}
info.innerText = `Menampilkan ${data.from || 0} sampai ${data.to || 0} dari ${data.total || 0} data`;
}
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 current = Number(data.current_page || 1);
const last = Number(data.last_page || 1);
const total = Number(data.total || 0);
const pageBtn = document.createElement('button');
pageBtn.className = 'page-link-custom active';
pageBtn.innerText = data.current_page;
controls.appendChild(pageBtn);
if (total === 0) return;
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);
const createButton = (label, page, disabled = false, active = false) => {
const btn = document.createElement('button');
btn.className = `page-link-custom ${active ? 'active' : ''}`;
btn.innerHTML = label;
btn.disabled = disabled;
btn.onclick = () => {
if (!disabled && !active) fetchAlumni(page);
};
return btn;
};
controls.appendChild(createButton('<i class="fas fa-chevron-left"></i>', current - 1, current === 1));
let pages = [];
if (last <= 5) {
for (let i = 1; i <= last; i++) pages.push(i);
} else {
pages.push(1);
if (current > 3) pages.push('...');
const start = Math.max(2, current - 1);
const end = Math.min(last - 1, current + 1);
for (let i = start; i <= end; i++) pages.push(i);
if (current < last - 2) pages.push('...');
pages.push(last);
}
pages.forEach(page => {
if (page === '...') {
controls.appendChild(createButton('...', current, true));
} else {
controls.appendChild(createButton(page, page, false, page === current));
}
});
controls.appendChild(createButton('<i class="fas fa-chevron-right"></i>', current + 1, current === last));
}
async function deleteAlumni(id) {
const res = await Swal.fire({
const result = await Swal.fire({
title: 'Hapus Data?',
text: "Data alumni akan dihapus secara permanen!",
text: 'Data alumni akan dihapus secara permanen!',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#ef4444',
confirmButtonText: 'Ya, Hapus'
cancelButtonColor: '#64748b',
confirmButtonText: 'Ya, Hapus',
cancelButtonText: 'Batal'
});
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();
if (!result.isConfirmed) return;
const token = localStorage.getItem('access_token');
try {
const response = await fetch('/api/admin/alumni/' + id, {
method: 'DELETE',
headers: {
'Authorization': 'Bearer ' + token,
'Accept': 'application/json'
}
} catch (error) { Swal.fire('Error!', 'Koneksi gagal.', 'error'); }
});
if (response.ok) {
Swal.fire('Terhapus!', 'Data alumni berhasil dihapus.', 'success');
fetchAlumni(currentPage);
} else {
Swal.fire('Gagal!', 'Data alumni gagal dihapus.', 'error');
}
} catch (error) {
Swal.fire('Error!', 'Koneksi ke server gagal.', 'error');
}
}
document.addEventListener('DOMContentLoaded', () => fetchAlumni(1));
document.addEventListener('DOMContentLoaded', function () {
fetchAlumni(1);
document.getElementById('searchInput').addEventListener('input', function () {
clearTimeout(searchTimer);
searchTimer = setTimeout(() => {
currentSearch = this.value.trim();
fetchAlumni(1);
}, 350);
});
document.getElementById('perPageSelect').addEventListener('change', function () {
perPage = this.value;
fetchAlumni(1);
});
});
</script>
@endsection

View File

@ -1,70 +1,284 @@
@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>
<link rel="stylesheet" href="{{ asset('css/dashboard-admin.css') }}">
<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="dashboard-modern">
<section class="dashboard-hero">
<div class="dashboard-hero-content">
<div>
<div class="hero-label">
<i class="fas fa-chart-pie"></i>
Dashboard Admin
</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>
<h2 class="hero-title">Overview Statistik Aplikasi</h2>
<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>
<p class="hero-subtitle">
Pantau jumlah siswa, rekomendasi yang sudah diproses, rata-rata tingkat keyakinan prediksi,
serta distribusi jurusan yang paling banyak muncul pada sistem rekomendasi jurusan kuliah SMAN 4 Jember.
</p>
</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 class="hero-date-card">
<span>Terakhir diperbarui</span>
<strong id="last-updated">Memuat...</strong>
</div>
</div>
</div>
</section>
<section class="modern-stats-grid">
<div class="modern-stat-card">
<div class="stat-card-content">
<div>
<p class="stat-label">Total Siswa</p>
<h3 class="stat-value" id="stat-total-siswa">0</h3>
<p class="stat-note">Jumlah akun siswa yang terdaftar.</p>
</div>
<div class="modern-stat-icon icon-blue">
<i class="fas fa-user-graduate"></i>
</div>
</div>
</div>
<div class="modern-stat-card">
<div class="stat-card-content">
<div>
<p class="stat-label">Rekomendasi Diproses</p>
<h3 class="stat-value" id="stat-total-rekomendasi">0</h3>
<p class="stat-note">Jumlah riwayat rekomendasi yang tersimpan.</p>
</div>
<div class="modern-stat-icon icon-green">
<i class="fas fa-check-circle"></i>
</div>
</div>
</div>
<div class="modern-stat-card">
<div class="stat-card-content">
<div>
<p class="stat-label">Rata-rata Keyakinan Prediksi</p>
<h3 class="stat-value" id="stat-rata-akurasi">0%</h3>
<p class="stat-note" id="akurasi-status">Menunggu data keyakinan prediksi.</p>
</div>
<div class="accuracy-ring" id="accuracy-ring">
<div class="accuracy-ring-inner" id="accuracy-ring-text">0%</div>
</div>
</div>
</div>
</section>
<section class="dashboard-card dashboard-chart-full">
<div class="card-header-modern">
<div>
<h5 class="card-title-modern">Distribusi Rekomendasi Jurusan</h5>
<p class="card-subtitle-modern">
Grafik ini menampilkan jumlah hasil rekomendasi berdasarkan jurusan yang muncul dari riwayat prediksi siswa.
Detail siswa dan hasil lengkap dapat dilihat pada menu <b>Riwayat Prediksi</b>.
</p>
</div>
<span class="chart-badge">
<i class="fas fa-chart-column me-1"></i>
Grafik Jurusan
</span>
</div>
<div class="chart-wrapper-modern">
<canvas id="distribusiChart" style="display: none;"></canvas>
<div id="chart-placeholder" class="dashboard-placeholder">
<div>
<i class="fas fa-chart-pie"></i>
<p>Memuat grafik distribusi...</p>
</div>
</div>
</div>
</section>
</div>
@push('scripts')
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
document.addEventListener('DOMContentLoaded', async function() {
let distribusiChartInstance = null;
function setText(id, value) {
const element = document.getElementById(id);
if (element) {
element.innerText = value;
}
}
function formatNumber(value) {
const number = Number(value || 0);
return new Intl.NumberFormat('id-ID').format(number);
}
function formatPercent(value) {
if (value === null || value === undefined || value === '') {
return '0%';
}
const number = Number(value);
if (Number.isNaN(number)) {
return '0%';
}
return `${number.toFixed(number % 1 === 0 ? 0 : 2)}%`;
}
function updateAccuracyRing(value) {
const ring = document.getElementById('accuracy-ring');
const ringText = document.getElementById('accuracy-ring-text');
const number = Number(value || 0);
const degree = Math.max(0, Math.min(100, number)) * 3.6;
if (ring) {
ring.style.background = `conic-gradient(#2D7A47 ${degree}deg, #e2e8f0 ${degree}deg)`;
}
if (ringText) {
ringText.innerText = formatPercent(number);
}
}
function updateLastUpdated() {
const now = new Date();
setText('last-updated', now.toLocaleString('id-ID', {
day: '2-digit',
month: 'short',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
}));
}
function renderDistribusiChart(distribusi) {
const placeholder = document.getElementById('chart-placeholder');
const canvas = document.getElementById('distribusiChart');
if (!placeholder || !canvas) {
return;
}
if (!distribusi || distribusi.length === 0) {
placeholder.style.display = 'flex';
canvas.style.display = 'none';
placeholder.querySelector('p').innerText = 'Belum ada data riwayat rekomendasi.';
return;
}
placeholder.style.display = 'none';
canvas.style.display = 'block';
const labels = distribusi.map(item => item.jurusan || 'Tidak diketahui');
const counts = distribusi.map(item => Number(item.total || 0));
const ctx = canvas.getContext('2d');
if (distribusiChartInstance) {
distribusiChartInstance.destroy();
}
const gradient = ctx.createLinearGradient(0, 0, 0, 360);
gradient.addColorStop(0, 'rgba(45, 122, 71, 0.95)');
gradient.addColorStop(1, 'rgba(45, 122, 71, 0.55)');
distribusiChartInstance = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'Jumlah Rekomendasi',
data: counts,
backgroundColor: gradient,
borderColor: 'rgba(45, 122, 71, 1)',
borderWidth: 1,
borderRadius: 14,
borderSkipped: false,
maxBarThickness: 74
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
layout: {
padding: {
top: 12,
right: 12,
bottom: 0,
left: 4
}
},
plugins: {
legend: {
display: false
},
tooltip: {
backgroundColor: '#0f172a',
titleColor: '#ffffff',
bodyColor: '#e2e8f0',
padding: 12,
cornerRadius: 12,
callbacks: {
label: function(context) {
return ` ${context.raw} rekomendasi`;
}
}
}
},
scales: {
x: {
grid: {
display: false
},
ticks: {
color: '#64748b',
font: {
size: 11,
weight: '700'
},
maxRotation: 0,
minRotation: 0
}
},
y: {
beginAtZero: true,
grid: {
color: '#edf2f7'
},
ticks: {
color: '#64748b',
precision: 0,
stepSize: 1,
font: {
size: 11,
weight: '700'
}
}
}
}
}
});
}
async function loadDashboardStats() {
const token = localStorage.getItem('access_token');
if (!token) {
window.location.href = '/login';
return;
}
updateLastUpdated();
try {
const response = await fetch('/api/admin/dashboard-stats', {
headers: {
@ -74,65 +288,42 @@
});
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.';
}
if (!response.ok || !result.success) {
throw new Error(result.message || 'Gagal memuat data dashboard.');
}
const data = result.data || {};
const rataAkurasi = data.rata_rata_akurasi;
setText('stat-total-siswa', formatNumber(data.total_siswa));
setText('stat-total-rekomendasi', formatNumber(data.rekomendasi_diproses));
setText('stat-rata-akurasi', formatPercent(rataAkurasi));
setText('akurasi-status', data.akurasi_status || 'Data keyakinan prediksi berhasil dimuat.');
updateAccuracyRing(rataAkurasi || 0);
renderDistribusiChart(data.distribusi_jurusan || []);
} catch (error) {
console.error('Gagal memuat data dashboard:', error);
document.getElementById('chart-placeholder').querySelector('p').innerText = 'Gagal memuat data.';
setText('akurasi-status', 'Gagal memuat data dashboard.');
updateAccuracyRing(0);
const placeholder = document.getElementById('chart-placeholder');
const canvas = document.getElementById('distribusiChart');
if (placeholder) {
placeholder.style.display = 'flex';
placeholder.querySelector('p').innerText = 'Gagal memuat grafik distribusi.';
}
if (canvas) {
canvas.style.display = 'none';
}
}
});
}
document.addEventListener('DOMContentLoaded', loadDashboardStats);
</script>
@endpush
@endsection

View File

@ -5,69 +5,158 @@
<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 class="master-page-header">
<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>
<h4 class="master-page-title">Data Guru BK</h4>
<p class="master-page-subtitle">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 href="{{ route('admin.guru.create') }}" class="btn-add-modern">
<i class="fas fa-plus"></i>
Tambah Guru BK
</a>
</div>
<div class="table-container-modern shadow-sm">
<div class="table-toolbar">
<div class="search-wrapper-modern">
<i class="fas fa-search"></i>
<input type="text" id="searchInput" class="search-input-modern" placeholder="Cari nama guru atau ID guru...">
</div>
<div class="per-page-wrapper">
<span>Tampilkan</span>
<select id="perPageSelect" class="per-page-select">
<option value="7" selected>7 data</option>
<option value="5">5 data</option>
</select>
</div>
</div>
<div class="table-responsive">
<table class="table table-modern mb-0">
<thead>
<tr>
<th class="text-center" width="100">ID Guru</th>
<th class="text-center" width="70">No</th>
<th class="text-center" width="120">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>
<tr>
<td colspan="4" class="loading-state-modern">
<i class="fas fa-spinner fa-spin me-2"></i> Memuat data guru...
</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 class="d-flex gap-1 flex-wrap" id="pagination-controls"></div>
</div>
</div>
</div>
<script>
let currentPage = 1;
let currentSearch = '';
let perPage = 7;
let searchTimer = null;
function escapeHtml(value) {
if (value === null || value === undefined || value === '') return '-';
return String(value)
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#039;');
}
function getInitial(name) {
const cleanName = name || 'G';
return cleanName.charAt(0).toUpperCase();
}
function showLoading() {
document.getElementById('guru-table-body').innerHTML = `
<tr>
<td colspan="4" class="loading-state-modern">
<i class="fas fa-spinner fa-spin me-2"></i> Memuat data guru...
</td>
</tr>
`;
}
async function fetchGuru(page = 1) {
const token = localStorage.getItem('access_token');
const tbody = document.getElementById('guru-table-body');
if (!token) {
window.location.href = '/login';
return;
}
currentPage = page;
showLoading();
const params = new URLSearchParams({
page: page,
per_page: perPage
});
if (currentSearch !== '') {
params.append('search', currentSearch);
}
try {
const response = await fetch(`/api/admin/guru?page=${page}`, {
headers: {
const response = await fetch(`/api/admin/guru?${params.toString()}`, {
headers: {
'Authorization': 'Bearer ' + token,
'Accept': 'application/json'
}
});
const result = await response.json();
const tbody = document.getElementById('guru-table-body');
if (!response.ok || result.success === false) {
throw new Error(result.message || 'Gagal memuat data guru.');
}
const data = result.data || [];
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>';
if (data.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="4" class="empty-state-modern">
<i class="fas fa-folder-open"></i>
Tidak ada data guru yang ditemukan.
</td>
</tr>
`;
updatePaginationInfo(null);
renderPagination({ current_page: 1, last_page: 1, total: 0 });
return;
}
result.data.forEach((item) => {
data.forEach((item, index) => {
tbody.innerHTML += `
<tr>
<td class="text-center fw-bold text-muted">${item.id_guru}</td>
<td class="text-center text-muted fw-bold">${(result.from || 1) + index}</td>
<td class="text-center fw-bold text-muted">${escapeHtml(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 class="table-user-cell">
<div class="avatar-circle orange">${getInitial(item.nama_guru)}</div>
<div>
<div class="fw-bold text-dark">${escapeHtml(item.nama_guru)}</div>
<div class="text-cell-muted">Guru Bimbingan Konseling</div>
</div>
</div>
</td>
<td class="text-center">
@ -80,66 +169,134 @@
</button>
</div>
</td>
</tr>`;
</tr>
`;
});
document.getElementById('pagination-info').innerText =
`Menampilkan ${result.from || 0} sampai ${result.to || 0} dari ${result.total || 0} data`;
updatePaginationInfo(result);
renderPagination(result);
} catch (error) {
tbody.innerHTML = '<tr><td colspan="3" class="text-center text-danger">Gagal memuat data.</td></tr>';
} catch (error) {
tbody.innerHTML = `
<tr>
<td colspan="4" class="empty-state-modern text-danger">
<i class="fas fa-exclamation-circle"></i>
Gagal memuat data guru.
</td>
</tr>
`;
}
}
function updatePaginationInfo(data) {
const info = document.getElementById('pagination-info');
if (!data || !data.total) {
info.innerText = 'Menampilkan 0 sampai 0 dari 0 data';
return;
}
info.innerText = `Menampilkan ${data.from || 0} sampai ${data.to || 0} dari ${data.total || 0} data`;
}
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 current = Number(data.current_page || 1);
const last = Number(data.last_page || 1);
const total = Number(data.total || 0);
const pageBtn = document.createElement('button');
pageBtn.className = 'page-link-custom active';
pageBtn.innerText = data.current_page || 1;
controls.appendChild(pageBtn);
if (total === 0) return;
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);
const createButton = (label, page, disabled = false, active = false) => {
const btn = document.createElement('button');
btn.className = `page-link-custom ${active ? 'active' : ''}`;
btn.innerHTML = label;
btn.disabled = disabled;
btn.onclick = () => {
if (!disabled && !active) fetchGuru(page);
};
return btn;
};
controls.appendChild(createButton('<i class="fas fa-chevron-left"></i>', current - 1, current === 1));
let pages = [];
if (last <= 5) {
for (let i = 1; i <= last; i++) pages.push(i);
} else {
pages.push(1);
if (current > 3) pages.push('...');
const start = Math.max(2, current - 1);
const end = Math.min(last - 1, current + 1);
for (let i = start; i <= end; i++) pages.push(i);
if (current < last - 2) pages.push('...');
pages.push(last);
}
pages.forEach(page => {
if (page === '...') {
controls.appendChild(createButton('...', current, true));
} else {
controls.appendChild(createButton(page, page, false, page === current));
}
});
controls.appendChild(createButton('<i class="fas fa-chevron-right"></i>', current + 1, current === last));
}
async function deleteGuru(id) {
const res = await Swal.fire({
const result = await Swal.fire({
title: 'Hapus Guru?',
text: "Data guru akan dihapus permanen!",
text: 'Data guru akan dihapus permanen!',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#ef4444',
confirmButtonText: 'Ya, Hapus'
cancelButtonColor: '#64748b',
confirmButtonText: 'Ya, Hapus',
cancelButtonText: 'Batal'
});
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();
if (!result.isConfirmed) return;
const token = localStorage.getItem('access_token');
try {
const response = await fetch('/api/admin/guru/' + id, {
method: 'DELETE',
headers: {
'Authorization': 'Bearer ' + token,
'Accept': 'application/json'
}
} catch (error) { Swal.fire('Error!', 'Koneksi gagal.', 'error'); }
});
if (response.ok) {
Swal.fire('Terhapus!', 'Data guru berhasil dihapus.', 'success');
fetchGuru(currentPage);
} else {
Swal.fire('Gagal!', 'Data guru gagal dihapus.', 'error');
}
} catch (error) {
Swal.fire('Error!', 'Koneksi ke server gagal.', 'error');
}
}
document.addEventListener('DOMContentLoaded', () => fetchGuru(1));
document.addEventListener('DOMContentLoaded', function () {
fetchGuru(1);
document.getElementById('searchInput').addEventListener('input', function () {
clearTimeout(searchTimer);
searchTimer = setTimeout(() => {
currentSearch = this.value.trim();
fetchGuru(1);
}, 350);
});
document.getElementById('perPageSelect').addEventListener('change', function () {
perPage = this.value;
fetchGuru(1);
});
});
</script>
@endsection

View File

@ -5,22 +5,39 @@
<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 class="master-page-header">
<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>
<h4 class="master-page-title">Data Master Jurusan</h4>
<p class="master-page-subtitle">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 href="{{ route('admin.jurusan.create') }}" class="btn-add-modern">
<i class="fas fa-plus"></i>
Tambah Jurusan
</a>
</div>
<div class="table-container-modern shadow-sm">
<div class="table-toolbar">
<div class="search-wrapper-modern">
<i class="fas fa-search"></i>
<input type="text" id="searchInput" class="search-input-modern" placeholder="Cari nama jurusan, rumpun, atau prospek karir...">
</div>
<div class="per-page-wrapper">
<span>Tampilkan</span>
<select id="perPageSelect" class="per-page-select">
<option value="7" selected>7 data</option>
<option value="5">5 data</option>
</select>
</div>
</div>
<div class="table-responsive">
<table class="table table-modern mb-0">
<thead>
<tr>
<th class="text-center" width="60">No</th>
<th class="text-center" width="70">No</th>
<th>Nama Jurusan</th>
<th class="text-center">Rumpun</th>
<th>Prospek Karir</th>
@ -28,54 +45,124 @@
</tr>
</thead>
<tbody id="jurusan-table-body">
<tr><td colspan="5" class="text-center py-5 text-muted">Memuat data...</td></tr>
<tr>
<td colspan="5" class="loading-state-modern">
<i class="fas fa-spinner fa-spin me-2"></i> Memuat data jurusan...
</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 class="d-flex gap-1 flex-wrap" id="pagination-controls"></div>
</div>
</div>
</div>
<script>
let currentPage = 1;
let currentSearch = '';
let perPage = 7;
let searchTimer = null;
function escapeHtml(value) {
if (value === null || value === undefined || value === '') return '-';
return String(value)
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#039;');
}
function showLoading() {
document.getElementById('jurusan-table-body').innerHTML = `
<tr>
<td colspan="5" class="loading-state-modern">
<i class="fas fa-spinner fa-spin me-2"></i> Memuat data jurusan...
</td>
</tr>
`;
}
async function fetchJurusan(page = 1) {
const token = localStorage.getItem('access_token');
const tbody = document.getElementById('jurusan-table-body');
if (!token) {
window.location.href = '/login';
return;
}
currentPage = page;
showLoading();
const params = new URLSearchParams({
page: page,
per_page: perPage
});
if (currentSearch !== '') {
params.append('search', currentSearch);
}
try {
const response = await fetch(`/api/admin/jurusan?page=${page}`, {
headers: {
const response = await fetch(`/api/admin/jurusan?${params.toString()}`, {
headers: {
'Authorization': 'Bearer ' + token,
'Accept': 'application/json'
}
});
const result = await response.json();
const tbody = document.getElementById('jurusan-table-body');
if (!response.ok || result.success === false) {
throw new Error(result.message || 'Gagal memuat data jurusan.');
}
const data = result.data || [];
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>';
if (data.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="5" class="empty-state-modern">
<i class="fas fa-folder-open"></i>
Tidak ada data jurusan yang ditemukan.
</td>
</tr>
`;
updatePaginationInfo(null);
renderPagination({ current_page: 1, last_page: 1, total: 0 });
return;
}
result.data.forEach((item, index) => {
data.forEach((item, index) => {
const rumpun = item.rumpun || '-';
const rumpunClass = rumpun.toLowerCase() === 'saintek' ? 'badge-saintek' : 'badge-soshum';
const rumpunClass = item.rumpun.toLowerCase() === 'saintek' ? 'badge-saintek' : 'badge-soshum';
const row = `
tbody.innerHTML += `
<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 class="text-center text-muted fw-bold">${(result.from || 1) + index}</td>
<td>
<div class="table-user-cell">
<div class="avatar-circle purple">
<i class="fas fa-graduation-cap"></i>
</div>
<div>
<div class="fw-bold text-dark text-uppercase">${escapeHtml(item.nama_jurusan)}</div>
<div class="text-cell-muted">Data master jurusan</div>
</div>
</div>
</td>
<td class="small text-muted">${item.prospek_karir || '-'}</td>
<td class="text-center">
<span class="badge-soft ${rumpunClass}">${escapeHtml(rumpun)}</span>
</td>
<td class="text-cell-muted">${escapeHtml(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">
@ -86,68 +173,134 @@
</button>
</div>
</td>
</tr>`;
tbody.innerHTML += row;
</tr>
`;
});
document.getElementById('pagination-info').innerText =
`Menampilkan ${result.from || 0} sampai ${result.to || 0} dari ${result.total || 0} data`;
updatePaginationInfo(result);
renderPagination(result);
} catch (error) {
tbody.innerHTML = '<tr><td colspan="5" class="text-center text-danger">Gagal memuat data.</td></tr>';
} catch (error) {
tbody.innerHTML = `
<tr>
<td colspan="5" class="empty-state-modern text-danger">
<i class="fas fa-exclamation-circle"></i>
Gagal memuat data jurusan.
</td>
</tr>
`;
}
}
function updatePaginationInfo(data) {
const info = document.getElementById('pagination-info');
if (!data || !data.total) {
info.innerText = 'Menampilkan 0 sampai 0 dari 0 data';
return;
}
info.innerText = `Menampilkan ${data.from || 0} sampai ${data.to || 0} dari ${data.total || 0} data`;
}
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 current = Number(data.current_page || 1);
const last = Number(data.last_page || 1);
const total = Number(data.total || 0);
const pageBtn = document.createElement('button');
pageBtn.className = 'page-link-custom active';
pageBtn.innerText = data.current_page;
controls.appendChild(pageBtn);
if (total === 0) return;
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);
const createButton = (label, page, disabled = false, active = false) => {
const btn = document.createElement('button');
btn.className = `page-link-custom ${active ? 'active' : ''}`;
btn.innerHTML = label;
btn.disabled = disabled;
btn.onclick = () => {
if (!disabled && !active) fetchJurusan(page);
};
return btn;
};
controls.appendChild(createButton('<i class="fas fa-chevron-left"></i>', current - 1, current === 1));
let pages = [];
if (last <= 5) {
for (let i = 1; i <= last; i++) pages.push(i);
} else {
pages.push(1);
if (current > 3) pages.push('...');
const start = Math.max(2, current - 1);
const end = Math.min(last - 1, current + 1);
for (let i = start; i <= end; i++) pages.push(i);
if (current < last - 2) pages.push('...');
pages.push(last);
}
pages.forEach(page => {
if (page === '...') {
controls.appendChild(createButton('...', current, true));
} else {
controls.appendChild(createButton(page, page, false, page === current));
}
});
controls.appendChild(createButton('<i class="fas fa-chevron-right"></i>', current + 1, current === last));
}
async function deleteJurusan(id) {
const res = await Swal.fire({
const result = await Swal.fire({
title: 'Hapus data?',
text: "Data yang dihapus tidak dapat dikembalikan!",
text: 'Data jurusan yang dihapus tidak dapat dikembalikan!',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#ef4444',
confirmButtonText: 'Ya, Hapus'
cancelButtonColor: '#64748b',
confirmButtonText: 'Ya, Hapus',
cancelButtonText: 'Batal'
});
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();
if (!result.isConfirmed) return;
const token = localStorage.getItem('access_token');
try {
const response = await fetch('/api/admin/jurusan/' + id, {
method: 'DELETE',
headers: {
'Authorization': 'Bearer ' + token,
'Accept': 'application/json'
}
} catch (error) { Swal.fire('Error!', 'Koneksi gagal.', 'error'); }
});
if (response.ok) {
Swal.fire('Terhapus!', 'Data jurusan berhasil dihapus.', 'success');
fetchJurusan(currentPage);
} else {
Swal.fire('Gagal!', 'Data jurusan gagal dihapus.', 'error');
}
} catch (error) {
Swal.fire('Error!', 'Koneksi ke server gagal.', 'error');
}
}
document.addEventListener('DOMContentLoaded', () => fetchJurusan(1));
document.addEventListener('DOMContentLoaded', function () {
fetchJurusan(1);
document.getElementById('searchInput').addEventListener('input', function () {
clearTimeout(searchTimer);
searchTimer = setTimeout(() => {
currentSearch = this.value.trim();
fetchJurusan(1);
}, 350);
});
document.getElementById('perPageSelect').addEventListener('change', function () {
perPage = this.value;
fetchJurusan(1);
});
});
</script>
@endsection

View File

@ -13,82 +13,186 @@
<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">
<style>
.sidebar-header {
text-align: center;
}
.sidebar-logo-wrapper {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16px;
}
/*
Style logo dibuat spesifik dan memakai !important
agar tidak tertimpa style dari file admin.css.
Bagian background putih, border-radius, padding, border, dan box-shadow
dihapus supaya logo tidak berbentuk oval.
*/
.sidebar-modern .sidebar-header .sidebar-logo-wrapper img.sidebar-school-logo {
width: 105px !important;
height: 105px !important;
object-fit: contain !important;
background: transparent !important;
background-color: transparent !important;
border-radius: 0 !important;
padding: 0 !important;
border: none !important;
box-shadow: none !important;
display: block !important;
margin: 0 auto !important;
filter: drop-shadow(0 6px 8px rgba(0, 0, 0, 0.22));
}
.sidebar-brand-text {
letter-spacing: 0.5px;
font-size: 1.05rem;
line-height: 1.3;
}
.sidebar-brand-sub {
letter-spacing: 0.4px;
font-size: 0.9rem;
}
.admin-avatar {
border-radius: 50%;
object-fit: cover;
}
</style>
@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>
<!-- Logo SMAN 4 Jember -->
<div class="sidebar-logo-wrapper">
<img src="{{ asset('image/smapa-removebg-preview.png') }}?v=3"
alt="Logo SMAN 4 Jember"
class="sidebar-school-logo"
onerror="this.onerror=null; this.src='https://ui-avatars.com/api/?name=S4&background=2D7A47&color=fff&rounded=true';">
</div>
<div class="sidebar-brand-text fw-bold text-white">
SMAN 4 JEMBER
</div>
<div class="sidebar-brand-sub small text-white" style="opacity: 0.85;">
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
<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
<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
<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
<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
<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
<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)
<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
<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 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">
<img src="https://ui-avatars.com/api/?name=Admin+BK&background=2D7A47&color=fff&rounded=true"
width="40"
height="40"
alt="Admin BK"
class="admin-avatar">
</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>

View File

@ -5,22 +5,39 @@
<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 class="master-page-header">
<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>
<h4 class="master-page-title">Data Master Prospek Kerja</h4>
<p class="master-page-subtitle">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 href="{{ route('admin.prospek-kerja.create') }}" class="btn-add-modern">
<i class="fas fa-plus"></i>
Tambah Prospek
</a>
</div>
<div class="table-container-modern shadow-sm">
<div class="table-toolbar">
<div class="search-wrapper-modern">
<i class="fas fa-search"></i>
<input type="text" id="searchInput" class="search-input-modern" placeholder="Cari profesi, jurusan, atau estimasi gaji...">
</div>
<div class="per-page-wrapper">
<span>Tampilkan</span>
<select id="perPageSelect" class="per-page-select">
<option value="7" selected>7 data</option>
<option value="5">5 data</option>
</select>
</div>
</div>
<div class="table-responsive">
<table class="table table-modern mb-0">
<thead>
<tr>
<th class="text-center" width="60">No</th>
<th class="text-center" width="70">No</th>
<th>Nama Profesi</th>
<th>Asal Jurusan</th>
<th>Estimasi Gaji</th>
@ -28,41 +45,123 @@
</tr>
</thead>
<tbody id="prospek-table-body">
<tr><td colspan="5" class="text-center py-5 text-muted">Memuat data...</td></tr>
<tr>
<td colspan="5" class="loading-state-modern">
<i class="fas fa-spinner fa-spin me-2"></i> Memuat data prospek kerja...
</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 flex-wrap" id="pagination-controls"></div>
</div>
</div>
</div>
<script>
async function fetchProspek() {
let currentPage = 1;
let currentSearch = '';
let perPage = 7;
let searchTimer = null;
function escapeHtml(value) {
if (value === null || value === undefined || value === '') return '-';
return String(value)
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#039;');
}
function showLoading() {
document.getElementById('prospek-table-body').innerHTML = `
<tr>
<td colspan="5" class="loading-state-modern">
<i class="fas fa-spinner fa-spin me-2"></i> Memuat data prospek kerja...
</td>
</tr>
`;
}
async function fetchProspek(page = 1) {
const token = localStorage.getItem('access_token');
const tbody = document.getElementById('prospek-table-body');
if (!token) {
window.location.href = '/login';
return;
}
currentPage = page;
showLoading();
const params = new URLSearchParams({
page: page,
per_page: perPage
});
if (currentSearch !== '') {
params.append('search', currentSearch);
}
try {
const response = await fetch(`/api/admin/prospek-kerja`, {
headers: {
const response = await fetch(`/api/admin/prospek-kerja?${params.toString()}`, {
headers: {
'Authorization': 'Bearer ' + token,
'Accept': 'application/json'
}
});
const result = await response.json();
const tbody = document.getElementById('prospek-table-body');
if (!response.ok || result.success === false) {
throw new Error(result.message || 'Gagal memuat data prospek kerja.');
}
const data = result.data || [];
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>';
if (data.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="5" class="empty-state-modern">
<i class="fas fa-folder-open"></i>
Tidak ada data prospek kerja yang ditemukan.
</td>
</tr>
`;
updatePaginationInfo(null);
renderPagination({ current_page: 1, last_page: 1, total: 0 });
return;
}
result.data.forEach((item, index) => {
const namaJurusan = item.jurusan ? item.jurusan.nama_jurusan : '<span class="text-danger">Jurusan Dihapus</span>';
const row = `
data.forEach((item, index) => {
const namaJurusan = item.jurusan ? escapeHtml(item.jurusan.nama_jurusan) : 'Jurusan Dihapus';
tbody.innerHTML += `
<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 text-muted fw-bold">${(result.from || 1) + index}</td>
<td>
<div class="table-user-cell">
<div class="avatar-circle blue">
<i class="fas fa-briefcase"></i>
</div>
<div>
<div class="fw-bold text-dark">${escapeHtml(item.nama_profesi)}</div>
<div class="text-cell-muted">Prospek kerja</div>
</div>
</div>
</td>
<td>
<span class="badge-soft badge-result">${namaJurusan}</span>
</td>
<td class="fw-bold text-success">${escapeHtml(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">
@ -73,39 +172,134 @@
</button>
</div>
</td>
</tr>`;
tbody.innerHTML += row;
</tr>
`;
});
} catch (error) {
document.getElementById('prospek-table-body').innerHTML = '<tr><td colspan="5" class="text-center text-danger">Gagal memuat data.</td></tr>';
updatePaginationInfo(result);
renderPagination(result);
} catch (error) {
tbody.innerHTML = `
<tr>
<td colspan="5" class="empty-state-modern text-danger">
<i class="fas fa-exclamation-circle"></i>
Gagal memuat data prospek kerja.
</td>
</tr>
`;
}
}
function updatePaginationInfo(data) {
const info = document.getElementById('pagination-info');
if (!data || !data.total) {
info.innerText = 'Menampilkan 0 sampai 0 dari 0 data';
return;
}
info.innerText = `Menampilkan ${data.from || 0} sampai ${data.to || 0} dari ${data.total || 0} data`;
}
function renderPagination(data) {
const controls = document.getElementById('pagination-controls');
controls.innerHTML = '';
const current = Number(data.current_page || 1);
const last = Number(data.last_page || 1);
const total = Number(data.total || 0);
if (total === 0) return;
const createButton = (label, page, disabled = false, active = false) => {
const btn = document.createElement('button');
btn.className = `page-link-custom ${active ? 'active' : ''}`;
btn.innerHTML = label;
btn.disabled = disabled;
btn.onclick = () => {
if (!disabled && !active) fetchProspek(page);
};
return btn;
};
controls.appendChild(createButton('<i class="fas fa-chevron-left"></i>', current - 1, current === 1));
let pages = [];
if (last <= 5) {
for (let i = 1; i <= last; i++) pages.push(i);
} else {
pages.push(1);
if (current > 3) pages.push('...');
const start = Math.max(2, current - 1);
const end = Math.min(last - 1, current + 1);
for (let i = start; i <= end; i++) pages.push(i);
if (current < last - 2) pages.push('...');
pages.push(last);
}
pages.forEach(page => {
if (page === '...') {
controls.appendChild(createButton('...', current, true));
} else {
controls.appendChild(createButton(page, page, false, page === current));
}
});
controls.appendChild(createButton('<i class="fas fa-chevron-right"></i>', current + 1, current === last));
}
async function deleteProspek(id) {
const res = await Swal.fire({
const result = await Swal.fire({
title: 'Hapus data?',
text: "Data yang dihapus tidak dapat dikembalikan!",
text: 'Data prospek kerja yang dihapus tidak dapat dikembalikan!',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#ef4444',
confirmButtonText: 'Ya, Hapus'
cancelButtonColor: '#64748b',
confirmButtonText: 'Ya, Hapus',
cancelButtonText: 'Batal'
});
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();
if (!result.isConfirmed) return;
const token = localStorage.getItem('access_token');
try {
const response = await fetch('/api/admin/prospek-kerja/' + id, {
method: 'DELETE',
headers: {
'Authorization': 'Bearer ' + token,
'Accept': 'application/json'
}
} catch (error) { Swal.fire('Error!', 'Koneksi gagal.', 'error'); }
});
if (response.ok) {
Swal.fire('Terhapus!', 'Data prospek kerja berhasil dihapus.', 'success');
fetchProspek(currentPage);
} else {
Swal.fire('Gagal!', 'Data prospek kerja gagal dihapus.', 'error');
}
} catch (error) {
Swal.fire('Error!', 'Koneksi ke server gagal.', 'error');
}
}
document.addEventListener('DOMContentLoaded', fetchProspek);
document.addEventListener('DOMContentLoaded', function () {
fetchProspek(1);
document.getElementById('searchInput').addEventListener('input', function () {
clearTimeout(searchTimer);
searchTimer = setTimeout(() => {
currentSearch = this.value.trim();
fetchProspek(1);
}, 350);
});
document.getElementById('perPageSelect').addEventListener('change', function () {
perPage = this.value;
fetchProspek(1);
});
});
</script>
@endsection

View File

@ -1,46 +1,94 @@
@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>
<link rel="stylesheet" href="{{ asset('css/fitur-admin.css') }}">
<link rel="stylesheet" href="{{ asset('css/riwayat-admin.css') }}">
<div class="dashboard-section">
<div class="riwayat-header">
<div>
<div class="riwayat-label">
<i class="fas fa-history"></i>
Riwayat Prediksi
</div>
<h4 class="master-page-title">Riwayat Prediksi Jurusan</h4>
<p class="master-page-subtitle">
Halaman ini menampilkan seluruh hasil rekomendasi siswa beserta informasi pendukung seperti waktu prediksi,
hasil rekomendasi, tingkat keyakinan, minat utama, dan rata-rata nilai.
</p>
</div>
</div>
<div class="table-container-modern shadow-sm">
<div class="table-toolbar">
<div class="search-wrapper-modern">
<i class="fas fa-search"></i>
<input type="text" id="searchInput" class="search-input-modern" placeholder="Cari nama siswa, email, atau hasil rekomendasi...">
</div>
<div class="per-page-wrapper">
<span>Tampilkan</span>
<select id="perPageSelect" class="per-page-select">
<option value="7" selected>7 data</option>
<option value="5">5 data</option>
</select>
</div>
</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">
<table class="table table-modern mb-0">
<thead>
<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>
<th class="text-center" width="70">No</th>
<th width="170">Waktu</th>
<th>Data Siswa</th>
<th>Hasil Rekomendasi</th>
<th>Ringkasan Input</th>
<th class="text-center" width="140">Keyakinan</th>
<th class="text-center" width="130">Aksi</th>
</tr>
</thead>
<tbody id="table-riwayat">
<tr>
<td colspan="4" class="text-center py-4 text-muted">
<td colspan="7" class="loading-state-modern">
<i class="fas fa-spinner fa-spin me-2"></i> Memuat data riwayat...
</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 flex-wrap" id="pagination-controls"></div>
</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-dialog modal-dialog-centered modal-lg">
<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>
<div class="modal-header border-bottom-0 px-4 pt-4">
<div>
<h5 class="modal-title fw-bold" id="detailModalLabel">Detail Prediksi Jurusan</h5>
<p class="text-muted small mb-0">Informasi ringkas dari hasil rekomendasi siswa.</p>
</div>
<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 class="modal-body px-4 pb-4" id="modal-body-content"></div>
<div class="modal-footer border-top-0 px-4 pb-4 pt-0">
<button type="button" class="btn btn-secondary px-4 rounded-pill shadow-none" data-bs-dismiss="modal">
Tutup
</button>
</div>
</div>
</div>
@ -48,20 +96,84 @@
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', async function() {
let currentPage = 1;
let currentSearch = '';
let perPage = 7;
let searchTimer = null;
function escapeHtml(value) {
if (value === null || value === undefined || value === '') return '-';
return String(value)
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#039;');
}
function formatPercent(value) {
if (value === null || value === undefined || value === '') {
return '-';
}
const number = Number(value);
if (Number.isNaN(number)) {
return '-';
}
return `${number.toFixed(number % 1 === 0 ? 0 : 2)}%`;
}
function formatRupiah(value) {
const number = Number(value || 0);
return new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
maximumFractionDigits: 0
}).format(number);
}
function getInitial(name) {
const cleanName = name || 'S';
return cleanName.charAt(0).toUpperCase();
}
function showLoading() {
document.getElementById('table-riwayat').innerHTML = `
<tr>
<td colspan="7" class="loading-state-modern">
<i class="fas fa-spinner fa-spin me-2"></i> Memuat data riwayat...
</td>
</tr>
`;
}
async function fetchRiwayat(page = 1) {
const token = localStorage.getItem('access_token');
// Cek sesi login
const tbody = document.getElementById('table-riwayat');
if (!token) {
window.location.href = '/login';
return;
}
const tbody = document.getElementById('table-riwayat');
currentPage = page;
showLoading();
const params = new URLSearchParams({
page: page,
per_page: perPage
});
if (currentSearch !== '') {
params.append('search', currentSearch);
}
try {
// Memanggil API riwayat
const response = await fetch('/api/admin/riwayat', {
const response = await fetch(`/api/admin/riwayat?${params.toString()}`, {
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/json'
@ -69,59 +181,187 @@
});
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 => `
if (!response.ok || result.success === false) {
throw new Error(result.message || 'Gagal memuat data riwayat.');
}
const data = result.data || [];
tbody.innerHTML = '';
if (data.length === 0) {
tbody.innerHTML = `
<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 colspan="7" class="empty-state-modern">
<i class="fas fa-folder-open"></i>
Tidak ada riwayat prediksi yang ditemukan.
</td>
</tr>
`;
updatePaginationInfo(null);
renderPagination({ current_page: 1, last_page: 1, total: 0 });
return;
}
data.forEach((item, index) => {
const confidence = formatPercent(item.akurasi_prediksi);
tbody.innerHTML += `
<tr>
<td class="text-center text-muted fw-bold">${(result.from || 1) + index}</td>
<td>
<div class="riwayat-date-main">${escapeHtml(item.tanggal)}</div>
<div class="riwayat-date-sub">Pukul ${escapeHtml(item.jam)}</div>
</td>
<td>
<div class="riwayat-user-cell">
<div class="riwayat-avatar">${escapeHtml(getInitial(item.nama_siswa))}</div>
<div>
<div class="riwayat-user-name">${escapeHtml(item.nama_siswa)}</div>
<div class="riwayat-user-email">${escapeHtml(item.email_siswa)}</div>
</div>
</div>
</td>
<td>
<span class="badge-soft badge-result">${escapeHtml(item.hasil)}</span>
</td>
<td>
<div class="riwayat-input-summary">
<span>Minat: ${escapeHtml(item.minat_utama)}</span>
<span>Rata-rata nilai: ${escapeHtml(item.rata_nilai)}</span>
</div>
</td>
<td class="text-center">
<button class="btn btn-sm btn-light px-3 rounded-pill text-primary fw-medium" onclick="showDetail(${item.id})">
<span class="confidence-badge">
<i class="fas fa-chart-line"></i>
${escapeHtml(confidence)}
</span>
</td>
<td class="text-center">
<button class="btn-detail" 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>';
}
});
`;
});
updatePaginationInfo(result);
renderPagination(result);
} catch (error) {
tbody.innerHTML = `
<tr>
<td colspan="7" class="empty-state-modern text-danger">
<i class="fas fa-exclamation-circle"></i>
Gagal memuat data riwayat.
</td>
</tr>
`;
}
}
function updatePaginationInfo(data) {
const info = document.getElementById('pagination-info');
if (!data || !data.total) {
info.innerText = 'Menampilkan 0 sampai 0 dari 0 data';
return;
}
info.innerText = `Menampilkan ${data.from || 0} sampai ${data.to || 0} dari ${data.total || 0} data`;
}
function renderPagination(data) {
const controls = document.getElementById('pagination-controls');
controls.innerHTML = '';
const current = Number(data.current_page || 1);
const last = Number(data.last_page || 1);
const total = Number(data.total || 0);
if (total === 0) return;
const createButton = (label, page, disabled = false, active = false) => {
const btn = document.createElement('button');
btn.className = `page-link-custom ${active ? 'active' : ''}`;
btn.innerHTML = label;
btn.disabled = disabled;
btn.onclick = () => {
if (!disabled && !active) fetchRiwayat(page);
};
return btn;
};
controls.appendChild(createButton('<i class="fas fa-chevron-left"></i>', current - 1, current === 1));
let pages = [];
if (last <= 5) {
for (let i = 1; i <= last; i++) pages.push(i);
} else {
pages.push(1);
if (current > 3) pages.push('...');
const start = Math.max(2, current - 1);
const end = Math.min(last - 1, current + 1);
for (let i = start; i <= end; i++) pages.push(i);
if (current < last - 2) pages.push('...');
pages.push(last);
}
pages.forEach(page => {
if (page === '...') {
controls.appendChild(createButton('...', current, true));
} else {
controls.appendChild(createButton(page, page, false, page === current));
}
});
controls.appendChild(createButton('<i class="fas fa-chevron-right"></i>', current + 1, current === last));
}
function renderObjectGrid(data, className) {
if (!data) return '';
return Object.entries(data).map(([label, value]) => {
return `
<div class="${className}">
<span>${escapeHtml(label)}</span>
<strong>${escapeHtml(value)}</strong>
</div>
`;
}).join('');
}
/**
* 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>
<div class="text-center py-5">
<i class="fas fa-spinner fa-spin text-success 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}`,
@ -133,34 +373,85 @@
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 class="detail-result-card">
<small>Hasil Rekomendasi Random Forest</small>
<h4>${escapeHtml(data.hasil_rekomendasi)}</h4>
</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 class="detail-grid">
<div class="detail-info-card">
<span>Nama Siswa</span>
<strong>${escapeHtml(data.nama_siswa)}</strong>
</div>
<div class="detail-info-card">
<span>Email Siswa</span>
<strong>${escapeHtml(data.email_siswa)}</strong>
</div>
<div class="detail-info-card">
<span>Waktu Prediksi</span>
<strong>${escapeHtml(data.tanggal)}</strong>
</div>
<div class="detail-info-card">
<span>Tingkat Keyakinan</span>
<strong>${escapeHtml(formatPercent(data.akurasi_prediksi))}</strong>
</div>
<div class="detail-info-card">
<span>Minat Utama</span>
<strong>${escapeHtml(data.minat_utama)}</strong>
</div>
<div class="detail-info-card">
<span>Rata-rata Nilai</span>
<strong>${escapeHtml(data.rata_nilai)}</strong>
</div>
<div class="detail-info-card">
<span>Kondisi Ekonomi</span>
<strong>${escapeHtml(data.kondisi_ekonomi)} · ${escapeHtml(formatRupiah(data.gaji_ortu))}</strong>
</div>
</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>
<h6 class="detail-section-title">Nilai Rapor Ringkas</h6>
<div class="mini-data-grid">
${renderObjectGrid(data.nilai_ringkas, 'mini-data-item')}
</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>
<h6 class="detail-section-title">Skor Minat RIASEC</h6>
<div class="mini-data-grid">
${renderObjectGrid(data.riasec, 'mini-data-item')}
</div>
`;
} else {
modalBody.innerHTML = `<div class="alert alert-danger m-0">Gagal memuat detail: ${result.message}</div>`;
modalBody.innerHTML = `<div class="alert alert-danger m-0">Gagal memuat detail: ${escapeHtml(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>`;
}
}
document.addEventListener('DOMContentLoaded', function () {
fetchRiwayat(1);
document.getElementById('searchInput').addEventListener('input', function () {
clearTimeout(searchTimer);
searchTimer = setTimeout(() => {
currentSearch = this.value.trim();
fetchRiwayat(1);
}, 350);
});
document.getElementById('perPageSelect').addEventListener('change', function () {
perPage = this.value;
fetchRiwayat(1);
});
});
</script>
@endpush
@endsection

View File

@ -5,22 +5,40 @@
<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 class="master-page-header">
<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>
<h4 class="master-page-title">Manajemen Data Siswa</h4>
<p class="master-page-subtitle">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 href="{{ route('admin.siswa.create') }}" class="btn-add-modern">
<i class="fas fa-plus"></i>
Tambah Siswa
</a>
</div>
<div class="table-container-modern shadow-sm">
<div class="table-toolbar">
<div class="search-wrapper-modern">
<i class="fas fa-search"></i>
<input type="text" id="searchInput" class="search-input-modern" placeholder="Cari nama siswa, NISN, kelas, atau peminatan...">
</div>
<div class="per-page-wrapper">
<span>Tampilkan</span>
<select id="perPageSelect" class="per-page-select">
<option value="7" selected>7 data</option>
<option value="5">5 data</option>
</select>
</div>
</div>
<div class="table-responsive">
<table class="table table-modern mb-0">
<thead>
<tr>
<th class="text-center" width="80">ID</th>
<th class="text-center" width="70">No</th>
<th class="text-center" width="110">ID Siswa</th>
<th>Nama Lengkap</th>
<th>NISN</th>
<th class="text-center">Kelas</th>
@ -28,51 +46,125 @@
</tr>
</thead>
<tbody id="siswa-table-body">
</tbody>
<tr>
<td colspan="6" class="loading-state-modern">
<i class="fas fa-spinner fa-spin me-2"></i> Memuat data siswa...
</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 class="d-flex gap-1 flex-wrap" id="pagination-controls"></div>
</div>
</div>
</div>
<script>
let currentPage = 1;
let currentSearch = '';
let perPage = 7;
let searchTimer = null;
function escapeHtml(value) {
if (value === null || value === undefined || value === '') return '-';
return String(value)
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#039;');
}
function getInitial(name) {
const cleanName = name || 'S';
return cleanName.charAt(0).toUpperCase();
}
function showLoading() {
document.getElementById('siswa-table-body').innerHTML = `
<tr>
<td colspan="6" class="loading-state-modern">
<i class="fas fa-spinner fa-spin me-2"></i> Memuat data siswa...
</td>
</tr>
`;
}
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>';
if (!token) {
window.location.href = '/login';
return;
}
currentPage = page;
showLoading();
const params = new URLSearchParams({
page: page,
per_page: perPage
});
if (currentSearch !== '') {
params.append('search', currentSearch);
}
try {
const response = await fetch(`/api/admin/siswa?page=${page}`, {
const response = await fetch(`/api/admin/siswa?${params.toString()}`, {
method: 'GET',
headers: {
headers: {
'Authorization': 'Bearer ' + token,
'Accept': 'application/json'
}
});
const result = await response.json();
if (!response.ok || result.success === false) {
throw new Error(result.message || 'Gagal memuat data siswa.');
}
const data = result.data || [];
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>';
if (data.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="6" class="empty-state-modern">
<i class="fas fa-folder-open"></i>
Tidak ada data siswa yang ditemukan.
</td>
</tr>
`;
updatePaginationInfo(null);
renderPagination({ current_page: 1, last_page: 1, total: 0 });
return;
}
result.data.forEach((item) => {
const row = `
data.forEach((item, index) => {
tbody.innerHTML += `
<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 text-muted fw-bold">${(result.from || 1) + index}</td>
<td class="text-center fw-bold text-muted">${escapeHtml(item.id_siswa)}</td>
<td>
<div class="table-user-cell">
<div class="avatar-circle">${getInitial(item.nama_lengkap)}</div>
<div>
<div class="fw-bold text-dark">${escapeHtml(item.nama_lengkap)}</div>
<div class="text-cell-muted">Data siswa aktif</div>
</div>
</div>
</td>
<td class="text-muted fw-semibold">${escapeHtml(item.nisn)}</td>
<td class="text-center">
<span class="badge rounded-pill badge-kelas">${item.kelas}</span>
<span class="badge-soft badge-kelas">${escapeHtml(item.kelas)}</span>
</td>
<td class="text-center">
<div class="opsi-wrapper">
@ -84,69 +176,134 @@
</button>
</div>
</td>
</tr>`;
tbody.innerHTML += row;
</tr>
`;
});
document.getElementById('pagination-info').innerText =
`Menampilkan ${result.from || 0} sampai ${result.to || 0} dari ${result.total || 0} data`;
updatePaginationInfo(result);
renderPagination(result);
} catch (error) {
tbody.innerHTML = '<tr><td colspan="5" class="text-center py-5 text-danger">Gagal memuat data.</td></tr>';
} catch (error) {
tbody.innerHTML = `
<tr>
<td colspan="6" class="empty-state-modern text-danger">
<i class="fas fa-exclamation-circle"></i>
Gagal memuat data siswa.
</td>
</tr>
`;
}
}
function updatePaginationInfo(data) {
const info = document.getElementById('pagination-info');
if (!data || !data.total) {
info.innerText = 'Menampilkan 0 sampai 0 dari 0 data';
return;
}
info.innerText = `Menampilkan ${data.from || 0} sampai ${data.to || 0} dari ${data.total || 0} data`;
}
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 current = Number(data.current_page || 1);
const last = Number(data.last_page || 1);
const total = Number(data.total || 0);
const pageBtn = document.createElement('button');
pageBtn.className = 'page-link-custom active';
pageBtn.innerText = data.current_page || 1;
controls.appendChild(pageBtn);
if (total === 0) return;
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);
const createButton = (label, page, disabled = false, active = false) => {
const btn = document.createElement('button');
btn.className = `page-link-custom ${active ? 'active' : ''}`;
btn.innerHTML = label;
btn.disabled = disabled;
btn.onclick = () => {
if (!disabled && !active) fetchSiswa(page);
};
return btn;
};
controls.appendChild(createButton('<i class="fas fa-chevron-left"></i>', current - 1, current === 1));
let pages = [];
if (last <= 5) {
for (let i = 1; i <= last; i++) pages.push(i);
} else {
pages.push(1);
if (current > 3) pages.push('...');
const start = Math.max(2, current - 1);
const end = Math.min(last - 1, current + 1);
for (let i = start; i <= end; i++) pages.push(i);
if (current < last - 2) pages.push('...');
pages.push(last);
}
pages.forEach(page => {
if (page === '...') {
controls.appendChild(createButton('...', current, true));
} else {
controls.appendChild(createButton(page, page, false, page === current));
}
});
controls.appendChild(createButton('<i class="fas fa-chevron-right"></i>', current + 1, current === last));
}
async function deleteSiswa(id) {
Swal.fire({
const result = await Swal.fire({
title: 'Hapus data?',
text: "Data akan dihapus permanen!",
text: 'Data siswa 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'); }
}
cancelButtonColor: '#64748b',
confirmButtonText: 'Ya, Hapus',
cancelButtonText: 'Batal'
});
if (!result.isConfirmed) return;
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 siswa berhasil dihapus.', 'success');
fetchSiswa(currentPage);
} else {
Swal.fire('Gagal!', 'Data siswa gagal dihapus.', 'error');
}
} catch (error) {
Swal.fire('Error!', 'Koneksi ke server gagal.', 'error');
}
}
document.addEventListener('DOMContentLoaded', () => fetchSiswa(1));
document.addEventListener('DOMContentLoaded', function () {
fetchSiswa(1);
document.getElementById('searchInput').addEventListener('input', function () {
clearTimeout(searchTimer);
searchTimer = setTimeout(() => {
currentSearch = this.value.trim();
fetchSiswa(1);
}, 350);
});
document.getElementById('perPageSelect').addEventListener('change', function () {
perPage = this.value;
fetchSiswa(1);
});
});
</script>
@endsection

View File

@ -1,179 +1,643 @@
<!DOCTYPE html>
<html lang="id">
<html lang="id" class="scroll-smooth">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ASGARD - Rekomendasi Jurusan SMAN 4 Jember</title>
<!-- Library -->
<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">
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<style>
body { font-family: 'Plus Jakarta Sans', sans-serif; }
body {
font-family: 'Plus Jakarta Sans', sans-serif;
}
html {
scroll-behavior: smooth;
scroll-padding-top: 95px;
}
[x-cloak] {
display: none !important;
}
/* Background halaman */
.soft-bg {
background:
radial-gradient(circle at top left, rgba(16, 185, 129, 0.14), transparent 35%),
radial-gradient(circle at bottom right, rgba(234, 179, 8, 0.10), transparent 35%),
#ffffff;
}
/* Teks gradasi */
.text-gradient {
background: linear-gradient(to right, #10b981, #059669);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
/* Jarak section */
.section-space {
padding-top: 90px;
padding-bottom: 90px;
}
@media (max-width: 768px) {
.section-space {
padding-top: 70px;
padding-bottom: 70px;
}
}
/* Animasi gambar */
.student-animation {
animation: floatStudent 3.5s ease-in-out infinite;
filter: drop-shadow(0 18px 18px rgba(6, 44, 34, 0.14));
}
@keyframes floatStudent {
0% {
transform: translateY(0) rotate(0deg);
}
50% {
transform: translateY(-16px) rotate(1.5deg);
}
100% {
transform: translateY(0) rotate(0deg);
}
}
/* Hover seluruh card */
.full-hover-card {
cursor: pointer;
transition: all 0.35s ease;
}
.full-hover-card:hover {
transform: translateY(-10px) scale(1.015);
box-shadow: 0 25px 50px rgba(6, 44, 34, 0.16);
}
/* Hover card putih */
.card-white:hover {
background-color: #ecfdf5 !important;
border-color: #22c55e !important;
}
/* Hover card hijau */
.card-dark:hover {
background-color: #022c22 !important;
border-color: #34d399 !important;
}
/* Hover card kuning */
.card-yellow:hover {
background-color: #facc15 !important;
border-color: #eab308 !important;
}
/* Efek isi card */
.number-box,
.card-icon,
.card-title,
.card-text {
transition: all 0.35s ease;
}
.full-hover-card:hover .number-box {
background-color: #10b981;
color: white;
transform: scale(1.08);
}
.full-hover-card:hover .card-icon {
transform: scale(1.15) rotate(-6deg);
}
.full-hover-card:hover .card-title {
color: #059669;
}
.full-hover-card:hover .card-text {
color: #334155;
}
.card-dark:hover .card-title {
color: #86efac;
}
.card-dark:hover .card-text {
color: #d1fae5;
}
.card-yellow:hover .card-title {
color: #064e3b;
}
.card-yellow:hover .card-text {
color: #064e3b;
}
/* Hover seluruh FAQ */
.faq-card {
cursor: pointer;
transition: all 0.35s ease;
}
.faq-card:hover {
background-color: #ecfdf5 !important;
border-color: #22c55e !important;
transform: translateY(-6px);
box-shadow: 0 22px 40px rgba(6, 44, 34, 0.12);
}
.faq-card:hover button {
color: #059669;
}
</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'">
<body
x-data="{ atTop: true, openMenu: false }"
@scroll.window="atTop = (window.pageYOffset > 50 ? false : true)"
class="flex flex-col min-h-screen bg-white"
>
<!-- NAVBAR -->
<nav
class="fixed w-full z-50 transition-all duration-500 px-6 py-4"
:class="atTop ? 'bg-transparent' : 'bg-white/95 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>
<!-- Logo -->
<a href="{{ route('landingpage') }}" 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>
</a>
<!-- Menu desktop -->
<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>
<a href="{{ route('landingpage') }}#tentang" class="hover:text-green-600 transition">
Tentang
</a>
<a href="{{ route('landingpage') }}#cara-kerja" class="hover:text-green-600 transition">
Cara Kerja
</a>
<a href="{{ route('landingpage') }}#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>
<!-- Login desktop -->
<div class="hidden md:block">
<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>
<!-- Tombol mobile -->
<button
@click="openMenu = !openMenu"
class="md:hidden text-emerald-900 font-black text-2xl"
type="button"
>
</button>
</div>
<!-- Menu mobile -->
<div
x-show="openMenu"
x-transition
x-cloak
class="md:hidden mt-4 bg-white rounded-3xl shadow-xl border border-green-100 p-5 space-y-4 text-sm font-bold uppercase tracking-widest text-emerald-900"
>
<a href="{{ route('landingpage') }}#tentang" @click="openMenu = false" class="block hover:text-green-600">
Tentang
</a>
<a href="{{ route('landingpage') }}#cara-kerja" @click="openMenu = false" class="block hover:text-green-600">
Cara Kerja
</a>
<a href="{{ route('landingpage') }}#faq" @click="openMenu = false" class="block hover:text-green-600">
FAQ
</a>
<a href="{{ route('login') }}" class="block text-center bg-emerald-900 text-white px-6 py-3 rounded-full">
LOGIN
</a>
</div>
</nav>
<main class="flex-grow">
<header class="pt-32 pb-20 overflow-hidden">
<!-- HERO -->
<header class="soft-bg pt-32 pb-24 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 class="text-5xl 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.
Sistem rekomendasi jurusan kuliah untuk membantu siswa SMAN 4 Jember menentukan pilihan jurusan berdasarkan profil siswa.
</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>
<a
href="{{ route('login') }}"
class="inline-block bg-green-600 text-white px-12 py-5 rounded-2xl font-bold uppercase text-xs tracking-widest shadow-2xl hover:bg-emerald-900 hover:-translate-y-1 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>
<h2 class="text-5xl font-black text-emerald-950 mb-6">
Kenali Potensimu Lebih Awal
</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."
ASGARD membantu siswa melihat kecocokan jurusan berdasarkan data nilai, minat, bakat, ekonomi, dan data pendukung alumni.
</p>
</div>
</div>
<div class="swiper-pagination"></div>
</div>
</div>
</header>
<section id="tentang" class="py-24 px-6">
<!-- TENTANG -->
<section id="tentang" class="section-space px-6 bg-white">
<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-green-600 font-black uppercase tracking-[0.3em] text-xs mb-4">
Tentang Sistem
</p>
<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.
<strong>ASGARD</strong> adalah sistem rekomendasi jurusan kuliah yang dirancang untuk membantu siswa SMAN 4 Jember dalam menentukan pilihan jurusan yang sesuai dengan profil siswa.
</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.
Sistem ini memanfaatkan data siswa sebagai dasar analisis, sehingga rekomendasi yang diberikan tidak hanya berdasarkan perkiraan, tetapi berdasarkan pola data yang telah diproses oleh sistem.
</p>
</div>
<!-- Gambar animasi -->
<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">
<img
src="https://illustrations.popsy.co/emerald/student-going-to-school.svg"
alt="Ilustrasi siswa"
class="student-animation w-full max-w-sm"
>
</div>
</div>
</section>
<section id="fitur" class="py-24 px-6 bg-green-50/50">
<!-- CARA KERJA -->
<section id="cara-kerja" class="section-space 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>
<!-- Judul cara kerja -->
<div class="text-center mb-14">
<p class="text-green-600 font-black uppercase tracking-[0.3em] text-xs mb-4">
Alur Sistem
</p>
<h2 class="text-4xl font-black text-emerald-950 italic uppercase">
Cara Kerja ASGARD
</h2>
<p class="max-w-2xl mx-auto text-emerald-900/60 font-medium mt-4">
Sistem bekerja dengan mengolah data profil siswa, kemudian menghasilkan rekomendasi jurusan kuliah yang paling sesuai.
</p>
</div>
<!-- Card alur sistem -->
<div class="grid md:grid-cols-3 gap-6 mb-16">
<div class="full-hover-card card-white bg-white rounded-3xl p-8 shadow-sm border border-emerald-100" data-aos="fade-up">
<div class="number-box w-12 h-12 rounded-2xl bg-green-100 text-green-700 flex items-center justify-center font-black mb-6">
1
</div>
<h3 class="card-title font-black text-emerald-950 text-xl mb-3">
Siswa Mengisi Data
</h3>
<p class="card-text text-slate-500 leading-relaxed text-sm">
Siswa mengisi data yang dibutuhkan, seperti nilai rapor, minat dan bakat, serta informasi pendukung lainnya.
</p>
</div>
<div class="full-hover-card card-dark bg-emerald-900 rounded-3xl p-8 shadow-xl text-white border border-emerald-900" data-aos="fade-up" data-aos-delay="100">
<div class="number-box w-12 h-12 rounded-2xl bg-green-500 text-white flex items-center justify-center font-black mb-6">
2
</div>
<h3 class="card-title font-black text-xl mb-3 text-white">
Sistem Memproses Data
</h3>
<p class="card-text text-emerald-100/70 leading-relaxed text-sm">
Data yang sudah dimasukkan akan diproses oleh sistem menggunakan model Random Forest untuk membaca pola kecocokan jurusan.
</p>
</div>
<div class="full-hover-card card-white bg-white rounded-3xl p-8 shadow-sm border border-emerald-100" data-aos="fade-up" data-aos-delay="200">
<div class="number-box w-12 h-12 rounded-2xl bg-green-100 text-green-700 flex items-center justify-center font-black mb-6">
3
</div>
<h3 class="card-title font-black text-emerald-950 text-xl mb-3">
Rekomendasi Ditampilkan
</h3>
<p class="card-text text-slate-500 leading-relaxed text-sm">
Sistem menampilkan hasil rekomendasi jurusan kuliah yang dapat digunakan siswa sebagai bahan pertimbangan.
</p>
</div>
</div>
<!-- Judul data analisis -->
<div class="text-center mb-10">
<h3 class="text-2xl font-black text-emerald-950 italic uppercase">
Data yang Dianalisis
</h3>
<p class="text-emerald-900/50 font-bold tracking-widest text-[10px] mt-2 uppercase">
Dasar Pembentukan Rekomendasi
</p>
</div>
<!-- Card data analisis -->
<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 class="full-hover-card card-white 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="card-icon 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>
<h4 class="card-title font-black text-emerald-950 mb-2 uppercase">
Nilai Rapor
</h4>
<p class="card-text text-sm text-slate-500 leading-relaxed">
Digunakan untuk melihat kemampuan akademik siswa pada beberapa mata pelajaran.
</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 class="full-hover-card card-dark bg-emerald-900 rounded-3xl p-10 flex flex-col justify-between h-80 text-white shadow-xl border border-emerald-900" data-aos="fade-up" data-aos-delay="100">
<span class="card-icon 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>
<h4 class="card-title font-black mb-2 uppercase text-green-400">
Minat & Bakat
</h4>
<p class="card-text text-sm text-emerald-100/60 leading-relaxed">
Digunakan untuk mengetahui kecenderungan minat siswa terhadap bidang tertentu.
</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 class="full-hover-card card-yellow bg-yellow-400 rounded-3xl p-10 flex flex-col justify-between h-80 shadow-sm border border-yellow-400" data-aos="fade-up" data-aos-delay="200">
<span class="card-icon 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>
<h4 class="card-title font-black text-emerald-900 mb-2 uppercase">
Ekonomi
</h4>
<p class="card-text text-sm text-emerald-900/70 leading-relaxed font-bold">
Digunakan sebagai data pendukung dalam menyesuaikan pilihan jurusan dengan kondisi 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 class="full-hover-card card-white 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="card-icon 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>
<h4 class="card-title font-black text-emerald-950 mb-2 uppercase">
Data Alumni
</h4>
<p class="card-text text-sm text-slate-500 leading-relaxed">
Digunakan sebagai data pendukung untuk melihat pola jurusan dari alumni sebelumnya.
</p>
</div>
</div>
</div>
</div>
</section>
<section id="faq" class="py-24 px-6">
<!-- FAQ -->
<section id="faq" class="section-space px-6 bg-white">
<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>
<!-- Judul FAQ -->
<div class="text-center mb-14">
<p class="text-green-600 font-black uppercase tracking-[0.3em] text-xs mb-4">
FAQ
</p>
<h2 class="text-4xl font-black text-emerald-950 italic">
Pertanyaan Umum
</h2>
</div>
<!-- Isi FAQ -->
<div class="space-y-4">
<div class="faq-card 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 transition"
type="button"
>
<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"
x-transition
class="px-8 pb-6 text-slate-500 leading-relaxed"
>
ASGARD memproses data profil siswa, seperti nilai rapor, minat dan bakat, ekonomi, serta data pendukung alumni. Data tersebut kemudian dianalisis oleh sistem untuk menghasilkan rekomendasi jurusan kuliah.
</div>
</div>
<div class="faq-card bg-white rounded-3xl border border-green-100 overflow-hidden shadow-sm">
<button
@click="active = (active === 2 ? null : 2)"
class="w-full px-8 py-6 text-left font-bold text-emerald-900 flex justify-between items-center transition"
type="button"
>
<span>Apakah hasil rekomendasi harus diikuti sepenuhnya?</span>
<svg class="w-5 h-5 transition-transform" :class="active === 2 ? '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 === 2"
x-transition
class="px-8 pb-6 text-slate-500 leading-relaxed"
>
Tidak. Hasil rekomendasi digunakan sebagai bahan pertimbangan. Siswa tetap dapat berdiskusi dengan guru BK, orang tua, atau wali kelas sebelum menentukan pilihan jurusan.
</div>
</div>
<div class="faq-card bg-white rounded-3xl border border-green-100 overflow-hidden shadow-sm">
<button
@click="active = (active === 3 ? null : 3)"
class="w-full px-8 py-6 text-left font-bold text-emerald-900 flex justify-between items-center transition"
type="button"
>
<span>Siapa yang dapat menggunakan sistem ini?</span>
<svg class="w-5 h-5 transition-transform" :class="active === 3 ? '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 === 3"
x-transition
class="px-8 pb-6 text-slate-500 leading-relaxed"
>
Sistem ini digunakan oleh siswa dan admin sekolah. Siswa dapat melihat rekomendasi jurusan, sedangkan admin dapat mengelola data yang dibutuhkan oleh sistem.
</div>
</div>
</div>
</div>
</section>
</main>
<!-- FOOTER -->
<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>
<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.
Sistem rekomendasi jurusan kuliah SMAN 4 Jember berbasis data.
</p>
</div>
<div>
<h5 class="font-bold text-xs uppercase tracking-widest text-emerald-400 mb-6 italic">Informasi</h5>
<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>
<li>
<a href="{{ route('panduan') }}" class="hover:text-white transition">
Panduan Sistem
</a>
</li>
<li>
<a href="{{ route('landingpage') }}#faq" class="hover:text-white transition">
FAQ
</a>
</li>
</ul>
</div>
<div>
<h5 class="font-bold text-xs uppercase tracking-widest text-emerald-400 mb-6 italic">Sekolah</h5>
<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>
<p class="text-[10px] font-medium uppercase tracking-[0.5em] text-slate-400">
© 2026 GALANG SEFIAN ADJI - POLITEKNIK NEGERI JEMBER
</p>
</div>
</footer>
<!-- Script animasi -->
<script src="https://unpkg.com/aos@2.3.1/dist/aos.js"></script>
<!-- Script slider -->
<script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js"></script>
<script>
AOS.init({ duration: 1000, once: true });
// Animasi saat scroll
AOS.init({
duration: 1000,
once: true
});
// Slider pada hero
new Swiper(".heroSwiper", {
pagination: { el: ".swiper-pagination", clickable: true },
autoplay: { delay: 4000 },
pagination: {
el: ".swiper-pagination",
clickable: true
},
autoplay: {
delay: 4000
},
loop: true,
});
</script>
</body>
</html>

View File

@ -4,153 +4,250 @@
<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; }
body {
font-family: 'Plus Jakarta Sans', sans-serif;
background-color: #f8fafc;
}
</style>
</head>
<body class="flex flex-col min-h-screen">
<!-- NAVBAR -->
<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>
<!-- Logo ASGARD -->
<a href="{{ url('/') }}" id="logo-link" class="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>
<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">
<span class="font-black text-xl tracking-tighter text-emerald-950 uppercase italic">
ASGARD.
</span>
</a>
<div class="flex items-center gap-4">
<div class="hidden md:flex items-center gap-4 text-[11px] font-bold uppercase tracking-widest">
<!-- Tombol kembali ke landing page: putih, hover hijau -->
<a href="{{ url('/') }}" id="landing-menu" class="bg-white text-emerald-700 border border-emerald-600 px-6 py-3 rounded-xl hover:bg-emerald-600 hover:text-white hover:-translate-y-0.5 transition-all duration-300 shadow-md shadow-emerald-900/10">
Kembali ke Landing Page
</a>
<!-- Tombol beranda muncul jika pengguna sudah login -->
<a href="#" id="beranda-menu" class="hidden bg-white text-emerald-700 border border-emerald-600 px-6 py-3 rounded-xl hover:bg-emerald-600 hover:text-white hover:-translate-y-0.5 transition-all duration-300 shadow-md shadow-emerald-900/10">
Beranda
</a>
<!-- Menu aktif halaman panduan -->
<!-- <a href="{{ route('panduan') }}" class="text-emerald-600 px-2 py-3">
Panduan
</a> -->
</div>
<!-- Tombol masuk sistem: hijau, hover putih -->
<a href="{{ route('login') }}" id="login-btn" class="bg-emerald-600 text-white border border-emerald-600 text-[11px] font-bold uppercase tracking-widest px-6 py-3 rounded-xl hover:bg-white hover:text-emerald-700 hover:-translate-y-0.5 transition-all duration-300 shadow-lg shadow-emerald-600/30">
Masuk Sistem
</a>
</div>
</div>
</nav>
<!-- CONTENT -->
<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>
<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">
<!-- Langkah 1 -->
<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 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>
<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>
<!-- Langkah 2 -->
<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 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>
<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.
Siswa dapat masuk ke sistem melalui menu <strong>Login</strong> menggunakan email/NISN dan kata sandi yang telah diberikan oleh Guru BK.
</p>
</div>
</div>
<!-- Langkah 3 -->
<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 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>
<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.
Langkah pertama setelah masuk adalah menginput rentang penghasilan orang tua. Sistem membutuhkan data ini untuk memetakan kelompok kemampuan ekonomi agar rekomendasi jurusan sesuai dengan kesiapan finansial.
</p>
</div>
</div>
<!-- Langkah 4 -->
<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 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>
<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.
Selanjutnya, siswa mengisi kuesioner minat dan bakat untuk membantu sistem mengetahui kecenderungan bidang yang sesuai dengan ketertarikan siswa.
</p>
</div>
</div>
<!-- Langkah 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">5</div>
<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>
<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.
Tahap input terakhir adalah mengisi rata-rata nilai dari 15 mata pelajaran selama semester 1 hingga semester 5 dengan akurat sebagai indikator kemampuan akademik siswa.
</p>
</div>
</div>
<!-- Langkah 6 -->
<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 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.
Sistem mulai menganalisis data ekonomi, minat dan bakat, serta nilai rapor siswa. Data tersebut kemudian diproses menggunakan algoritma Random Forest untuk menghasilkan rekomendasi jurusan.
</p>
</div>
</div>
<!-- Langkah 7 -->
<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 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>
<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.
Sistem akan menampilkan hasil berupa jurusan kuliah yang paling sesuai dengan profil siswa berdasarkan data yang telah diisi sebelumnya.
</p>
</div>
</div>
<!-- Langkah 8 -->
<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 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>
<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.
Siswa tidak hanya mendapatkan nama jurusan, tetapi juga dapat melihat informasi mengenai peluang kerja dan prospek karir dari jurusan yang direkomendasikan.
</p>
</div>
</div>
<!-- Langkah 9 -->
<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 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>
<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.
Hasil rekomendasi dapat digunakan sebagai bahan pertimbangan awal. Siswa tetap disarankan melakukan konsultasi dengan Guru BK untuk memantapkan pilihan jurusan kuliah.
</p>
</div>
</div>
@ -158,54 +255,164 @@
</div>
</div>
<!-- FOOTER -->
<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>
<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.
Sistem rekomendasi jurusan kuliah SMAN 4 Jember berbasis data.
</p>
</div>
<div>
<h5 class="font-bold text-xs uppercase tracking-widest text-emerald-400 mb-6">Informasi</h5>
<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>
<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>
<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>
<p class="text-[10px] font-medium uppercase tracking-[0.5em] text-slate-400">
© 2026 GALANG SEFIAN ADJI - POLITEKNIK NEGERI JEMBER
</p>
</div>
</footer>
<!-- SCRIPT NAVBAR LOGIN CHECK -->
<script>
document.addEventListener('DOMContentLoaded', function() {
document.addEventListener('DOMContentLoaded', function () {
const token = localStorage.getItem('access_token');
const berandaLinks = document.querySelectorAll('.beranda-link');
const role =
localStorage.getItem('role') ||
localStorage.getItem('user_role') ||
getRoleFromUserObject();
const logoLink = document.getElementById('logo-link');
const landingMenu = document.getElementById('landing-menu');
const berandaMenu = document.getElementById('beranda-menu');
const loginBtn = document.getElementById('login-btn');
const landingUrl = "{{ url('/') }}";
const loginUrl = "{{ route('login') }}";
// URL dashboard siswa
const siswaDashboardUrl = "{{ route('student.profile') }}";
// URL dashboard admin
// Jika dashboard admin kamu berbeda, cukup ubah bagian ini saja.
const adminDashboardUrl = "{{ url('/admin/dashboard') }}";
let dashboardUrl = siswaDashboardUrl;
if (role && role.toLowerCase() === 'admin') {
dashboardUrl = adminDashboardUrl;
}
if (token) {
berandaLinks.forEach(link => {
link.href = "{{ route('student.profile') }}";
});
// Jika sudah login:
// tombol kembali ke landing page disembunyikan
if (landingMenu) {
landingMenu.classList.add('hidden');
}
// menu beranda muncul dan menuju dashboard sesuai role
if (berandaMenu) {
berandaMenu.classList.remove('hidden');
berandaMenu.href = dashboardUrl;
}
// logo menuju dashboard
if (logoLink) {
logoLink.href = dashboardUrl;
}
// tombol masuk sistem berubah menjadi ke dashboard
if (loginBtn) {
loginBtn.innerText = "Ke Dashboard →";
loginBtn.href = "{{ route('student.profile') }}";
loginBtn.href = dashboardUrl;
loginBtn.className = "bg-emerald-600 text-white border border-emerald-600 text-[11px] font-bold uppercase tracking-widest px-6 py-3 rounded-xl hover:bg-white hover:text-emerald-700 hover:-translate-y-0.5 transition-all duration-300 shadow-lg shadow-emerald-600/30";
}
} else {
// Jika belum login:
// tombol kembali ke landing page ditampilkan
if (landingMenu) {
landingMenu.classList.remove('hidden');
landingMenu.href = landingUrl;
}
// menu beranda disembunyikan
if (berandaMenu) {
berandaMenu.classList.add('hidden');
berandaMenu.removeAttribute('href');
}
// logo menuju landing page
if (logoLink) {
logoLink.href = landingUrl;
}
// tombol masuk sistem menuju login
if (loginBtn) {
loginBtn.innerText = "Masuk Sistem →";
loginBtn.href = loginUrl;
loginBtn.className = "bg-emerald-600 text-white border border-emerald-600 text-[11px] font-bold uppercase tracking-widest px-6 py-3 rounded-xl hover:bg-white hover:text-emerald-700 hover:-translate-y-0.5 transition-all duration-300 shadow-lg shadow-emerald-600/30";
}
}
function getRoleFromUserObject() {
try {
const userData = localStorage.getItem('user');
if (!userData) {
return null;
}
const user = JSON.parse(userData);
return user.role || null;
} catch (error) {
return null;
}
}
});
</script>
</body>
</html>

View File

@ -4,144 +4,405 @@
<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">
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;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); }
body {
font-family: 'Plus Jakarta Sans', sans-serif;
background:
radial-gradient(circle at 8% 12%, rgba(16, 185, 129, 0.10), transparent 32%),
radial-gradient(circle at 92% 8%, rgba(59, 130, 246, 0.08), transparent 30%),
linear-gradient(180deg, #f8fafc 0%, #f3f7fb 48%, #f8fafc 100%);
}
nav {
box-shadow: 0 10px 35px rgba(15, 23, 42, 0.05);
}
nav a {
position: relative;
}
nav a::after {
content: "";
position: absolute;
left: 0;
bottom: -10px;
width: 0;
height: 2px;
border-radius: 999px;
background: #10b981;
transition: width 0.25s ease;
}
nav a:hover::after,
nav a.text-emerald-600::after {
width: 100%;
}
main > header {
background: rgba(255, 255, 255, 0.88);
border: 1px solid rgba(226, 232, 240, 0.9);
border-radius: 32px;
padding: 34px;
box-shadow: 0 18px 55px rgba(15, 23, 42, 0.06);
backdrop-filter: blur(18px);
}
main > header h1 {
letter-spacing: -0.045em;
}
.card-shadow {
box-shadow: 0 18px 50px rgba(15, 23, 42, 0.07);
}
.card-hover {
transition: transform 0.25s ease, box-shadow 0.25s ease, border-color 0.25s ease, background-color 0.25s ease;
}
.card-hover:hover {
transform: translateY(-4px);
box-shadow: 0 24px 60px rgba(15, 23, 42, 0.11);
border-color: rgba(16, 185, 129, 0.20);
}
main section.bg-white,
main section > div.bg-white,
main .bg-white.p-8,
main .bg-white.p-8.md\:p-10 {
background: rgba(255, 255, 255, 0.93);
backdrop-filter: blur(14px);
}
#activity-list a,
#activity-list-large a {
box-shadow: 0 6px 18px rgba(15, 23, 42, 0.035);
}
#activity-list a:hover,
#activity-list-large a:hover {
transform: translateX(4px);
}
#trendChart {
filter: drop-shadow(0 12px 24px rgba(16, 185, 129, 0.10));
}
footer {
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
}
@media (max-width: 768px) {
main > header {
padding: 24px;
border-radius: 26px;
}
}
</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">
<body class="flex flex-col min-h-screen text-slate-800">
<!-- Navbar -->
<nav class="sticky top-0 z-50 bg-white/95 backdrop-blur-md border-b border-slate-100 px-6 py-4">
<div class="max-w-7xl mx-auto flex justify-between items-center gap-4">
<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 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>
<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>
<!-- Menu ini membuka hasil rekomendasi terakhir -->
<a href="#" onclick="goToLastResult(event)" class="hover:text-emerald-600 transition">
Hasil 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="hidden sm: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>
<span id="student-name-label" class="text-[10px] font-bold text-emerald-700 uppercase">
Siswa
</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>
<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">
<main class="max-w-7xl mx-auto px-6 flex-grow py-10">
<!-- Header -->
<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>
<h1 class="text-4xl font-extrabold text-slate-900 tracking-tight">
Halo, Siswa SMAPA! 👋
</h1>
<p class="text-slate-500 font-medium mt-2 max-w-3xl">
Dashboard ini menampilkan ringkasan aktivitas rekomendasi jurusan, riwayat analisis, dan perkembangan nilai yang pernah kamu masukkan.
</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>
<!-- Statistik -->
<section 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 card-hover">
<h4 class="text-slate-400 text-xs font-bold uppercase tracking-widest mb-4">
Total Rekomendasi
</h4>
<p id="stat-total" class="text-6xl font-black text-emerald-600 tracking-tighter">
0
</p>
<p class="text-[12px] text-slate-500 mt-6 font-medium leading-relaxed">
Jumlah rekomendasi jurusan yang pernah diproses melalui sistem.
</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 class="bg-white p-8 rounded-3xl card-shadow border border-slate-100 card-hover">
<h4 class="text-slate-400 text-xs font-bold uppercase tracking-widest mb-4">
Rekomendasi Terakhir
</h4>
<p id="stat-last" class="text-lg font-extrabold text-slate-800 leading-relaxed">
Memuat data...
</p>
<p class="text-[12px] text-slate-500 mt-6 leading-relaxed">
Detail hasil terakhir dapat dibuka melalui menu <b>Hasil Rekomendasi</b> pada navbar.
</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>
<h4 class="text-slate-400 text-xs font-bold uppercase tracking-widest mb-4">
Riwayat 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>
<p class="text-xs text-slate-400 italic">
Belum ada riwayat rekomendasi.
</p>
</div>
</div>
</div>
</section>
<!-- Akses cepat -->
<section class="bg-white p-8 md:p-10 rounded-[2.5rem] card-shadow border border-slate-100 mb-10">
<div class="mb-8">
<h3 class="text-xl font-black text-slate-900">
Akses Cepat
</h3>
<p class="text-sm text-slate-500 mt-2">
Gunakan menu berikut untuk melanjutkan aktivitas pada sistem.
</p>
</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>
<a href="{{ route('student.prediction') }}"
class="flex items-center p-6 bg-blue-50 rounded-2xl border border-blue-100 card-hover">
<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-100">
📝
</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>
<h4 class="font-black text-blue-900 text-sm">
Cek Rekomendasi Baru
</h4>
<p class="text-[11px] text-blue-700 font-semibold mt-1">
Isi data kembali untuk mendapatkan hasil terbaru.
</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>
<a href="#" onclick="goToLastResult(event)"
class="flex items-center p-6 bg-emerald-50 rounded-2xl border border-emerald-100 card-hover">
<div class="w-12 h-12 bg-emerald-500 text-white rounded-xl flex items-center justify-center text-xl mr-5 shadow-lg shadow-emerald-100">
📄
</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>
<h4 class="font-black text-emerald-900 text-sm">
Hasil Rekomendasi
</h4>
<p class="text-[11px] text-emerald-700 font-semibold mt-1">
Buka kembali hasil rekomendasi terakhir.
</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>
<a href="#riwayat-section"
class="flex items-center p-6 bg-violet-50 rounded-2xl border border-violet-100 card-hover">
<div class="w-12 h-12 bg-violet-500 text-white rounded-xl flex items-center justify-center text-xl mr-5 shadow-lg shadow-violet-100">
🕘
</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>
<h4 class="font-black text-violet-900 text-sm">
Riwayat Rekomendasi
</h4>
<p class="text-[11px] text-violet-700 font-semibold mt-1">
Lihat daftar rekomendasi yang pernah dibuat.
</p>
</div>
</a>
</div>
</div>
</section>
<!-- Grafik -->
<section class="bg-white p-8 md:p-10 rounded-[2.5rem] card-shadow border border-slate-100 mb-10">
<h3 class="text-xl font-black text-slate-900 mb-2">
Perkembangan Rata-Rata Nilai
</h3>
<p class="text-sm text-slate-500 font-medium mb-8">
Grafik ini menampilkan perubahan rata-rata nilai berdasarkan data yang pernah kamu masukkan saat melakukan rekomendasi.
</p>
<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>
<p id="chart-placeholder" class="hidden text-center text-slate-400 italic text-sm py-20">
Grafik akan muncul setelah terdapat data rekomendasi.
</p>
</section>
<!-- Panduan -->
<section id="panduan-section" class="bg-white p-8 md:p-10 rounded-[2.5rem] card-shadow border border-slate-100 mb-10">
<div class="flex flex-col lg:flex-row gap-8">
<div class="lg:w-1/3">
<div class="w-14 h-14 rounded-2xl bg-emerald-100 text-emerald-700 flex items-center justify-center text-2xl mb-5">
📌
</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>
<h3 class="text-2xl font-black text-slate-900">
Panduan Membaca Hasil
</h3>
<p class="text-sm text-slate-500 mt-3 leading-relaxed">
Hasil rekomendasi digunakan sebagai bahan pertimbangan awal. Keputusan akhir tetap perlu disesuaikan dengan minat, kemampuan, dan arahan Guru BK.
</p>
</div>
<div class="lg:w-2/3 grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="rounded-3xl bg-slate-50 border border-slate-100 p-6">
<h4 class="font-extrabold text-slate-800 mb-3">
1. Periksa Data
</h4>
<p class="text-sm text-slate-500 leading-relaxed">
Pastikan nilai rapor, minat, dan kondisi ekonomi sudah sesuai dengan data sebenarnya.
</p>
</div>
<div class="rounded-3xl bg-slate-50 border border-slate-100 p-6">
<h4 class="font-extrabold text-slate-800 mb-3">
2. Pelajari Jurusan
</h4>
<p class="text-sm text-slate-500 leading-relaxed">
Cari informasi tentang mata kuliah, biaya kuliah, peluang kerja, dan kampus yang menyediakan jurusan tersebut.
</p>
</div>
<div class="rounded-3xl bg-slate-50 border border-slate-100 p-6">
<h4 class="font-extrabold text-slate-800 mb-3">
3. Konsultasi BK
</h4>
<p class="text-sm text-slate-500 leading-relaxed">
Diskusikan hasil rekomendasi dengan Guru BK agar pilihan jurusan lebih matang.
</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>
</section>
<!-- Riwayat -->
<section id="riwayat-section" class="bg-white p-8 md:p-10 rounded-[2.5rem] card-shadow border border-slate-100 mb-16">
<div class="mb-6">
<h3 class="text-xl font-black text-slate-900">
Riwayat Rekomendasi
</h3>
<p class="text-sm text-slate-500 mt-2">
Riwayat terbaru akan muncul pada daftar berikut.
</p>
</div>
<div id="activity-list-large" class="space-y-3">
<p class="text-sm text-slate-400 italic">
Belum ada riwayat rekomendasi.
</p>
</div>
</section>
</main>
<!-- Footer -->
<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>
<span class="font-black text-2xl tracking-tighter uppercase italic">
ASGARD.
</span>
<p class="text-slate-300 text-sm leading-relaxed max-w-xs">
Sistem pakar rekomendasi jurusan kuliah SMAN 4 Jember berbasis data-driven.
Sistem rekomendasi jurusan kuliah SMAN 4 Jember berbasis profil siswa dan data akademik.
</p>
</div>
<div>
<h5 class="font-bold text-xs uppercase tracking-widest text-emerald-400 mb-6">Informasi</h5>
<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>
<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>
<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>
@ -149,14 +410,118 @@
</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>
<p class="text-[10px] font-medium uppercase tracking-[0.35em] text-slate-400">
© 2026 GALANG SEFIAN ADJI - POLITEKNIK NEGERI JEMBER
</p>
</div>
</footer>
<script>
let trendChartInstance = null;
const routeLogin = @json(route('login'));
const routeDashboard = @json(route('student.profile'));
const routePrediction = @json(route('student.prediction'));
const routeAdminDashboard = @json(route('admin.dashboard'));
function getAccessToken() {
return localStorage.getItem('access_token');
}
function getUserRole() {
return localStorage.getItem('user_role');
}
function redirectByRole(role) {
if (role === 'admin') {
window.location.href = routeAdminDashboard;
return;
}
if (role === 'siswa') {
window.location.href = routeDashboard;
return;
}
window.location.href = routeLogin;
}
function getStoredLastResult() {
try {
const saved = localStorage.getItem('asgard_last_result');
return saved ? JSON.parse(saved) : null;
} catch (error) {
return null;
}
}
function saveStoredLastResult(data) {
if (!data || !data.jurusan) return;
const oldData = getStoredLastResult() || {};
const nextData = {
id: data.id || oldData.id || '',
jurusan: data.jurusan || oldData.jurusan || '',
tanggal: data.tanggal || oldData.tanggal || '',
url: data.url || oldData.url || ''
};
localStorage.setItem('asgard_last_result', JSON.stringify(nextData));
}
function goToLastResult(event) {
event.preventDefault();
const saved = getStoredLastResult();
if (saved && saved.url) {
window.location.href = saved.url;
return;
}
if (saved && saved.id) {
window.location.href = `/student/result/${saved.id}`;
return;
}
alert('Belum ada hasil rekomendasi yang bisa dibuka. Silakan lakukan cek rekomendasi terlebih dahulu.');
window.location.href = routePrediction;
}
function getHistoryId(item) {
if (!item || typeof item !== 'object') return '';
return item.id
|| item.riwayat_id
|| item.riwayat_rekomendasi_id
|| item.rekomendasi_id
|| item.result_id
|| item.hasil_id
|| '';
}
function makeResultUrl(id) {
return id ? `/student/result/${id}` : '';
}
function isValidRecommendationName(value) {
if (!value) return false;
const text = String(value).trim().toLowerCase();
return text !== ''
&& text !== '-'
&& text !== 'null'
&& text !== 'undefined'
&& !text.includes('belum');
}
async function handleLogout() {
const token = localStorage.getItem('access_token');
const token = getAccessToken();
if (token) {
try {
await fetch('/api/logout', {
@ -169,18 +534,171 @@
} catch (error) {
console.error('Logout error:', error);
}
localStorage.removeItem('access_token');
}
window.location.href = '/';
localStorage.removeItem('access_token');
localStorage.removeItem('user_role');
localStorage.removeItem('asgard_last_result');
window.location.href = routeLogin;
}
document.addEventListener('DOMContentLoaded', async function() {
const token = localStorage.getItem('access_token');
if (!token) {
window.location.href = '/login';
function renderActivityList(activities) {
const smallList = document.getElementById('activity-list');
const largeList = document.getElementById('activity-list-large');
if (!Array.isArray(activities) || activities.length === 0) {
smallList.innerHTML = `
<p class="text-xs text-slate-400 italic">
Belum ada riwayat rekomendasi.
</p>
`;
largeList.innerHTML = `
<p class="text-sm text-slate-400 italic">
Belum ada riwayat rekomendasi.
</p>
`;
return;
}
smallList.innerHTML = activities.slice(0, 5).map((act) => {
const id = getHistoryId(act);
const url = makeResultUrl(id);
const jurusan = act.jurusan || act.hasil_rekomendasi_jurusan || 'Rekomendasi Jurusan';
const tanggal = act.tanggal || act.created_at || '';
return `
<a href="${url || '#'}"
class="flex items-center justify-between bg-slate-50 hover:bg-emerald-50 p-3 rounded-xl border border-slate-100 hover:border-emerald-100 transition">
<div class="flex items-center gap-3">
<span class="text-lg">📄</span>
<span class="text-[11px] font-bold text-slate-700 uppercase tracking-tight">
${jurusan}
</span>
</div>
<span class="text-[9px] font-black text-slate-400 uppercase">
${tanggal}
</span>
</a>
`;
}).join('');
largeList.innerHTML = activities.map((act) => {
const id = getHistoryId(act);
const url = makeResultUrl(id);
const jurusan = act.jurusan || act.hasil_rekomendasi_jurusan || 'Rekomendasi Jurusan';
const tanggal = act.tanggal || act.created_at || 'Tanggal tidak tersedia';
return `
<a href="${url || '#'}"
class="flex flex-col md:flex-row md:items-center md:justify-between gap-3 rounded-2xl border border-slate-100 bg-slate-50 hover:bg-emerald-50 hover:border-emerald-100 p-5 transition">
<div class="flex items-center gap-4">
<div class="w-11 h-11 rounded-2xl bg-white flex items-center justify-center border border-slate-100">
📄
</div>
<div>
<h4 class="font-extrabold text-slate-800 uppercase">
${jurusan}
</h4>
<p class="text-xs text-slate-500 mt-1">
${tanggal}
</p>
</div>
</div>
<span class="text-xs font-bold text-emerald-700">
${url ? 'Lihat detail →' : 'Detail tidak tersedia'}
</span>
</a>
`;
}).join('');
}
function renderTrendChart(tren) {
const canvas = document.getElementById('trendChart');
const placeholder = document.getElementById('chart-placeholder');
if (!Array.isArray(tren) || tren.length === 0) {
canvas.classList.add('hidden');
placeholder.classList.remove('hidden');
return;
}
canvas.classList.remove('hidden');
placeholder.classList.add('hidden');
const ctx = canvas.getContext('2d');
if (trendChartInstance) {
trendChartInstance.destroy();
}
trendChartInstance = 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.12)',
fill: true,
tension: 0.4,
pointRadius: 5,
pointBackgroundColor: '#ffffff',
pointBorderColor: '#10b981',
pointBorderWidth: 3
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
},
scales: {
y: {
min: 0,
max: 100,
ticks: {
color: '#64748b'
},
grid: {
color: '#f1f5f9'
}
},
x: {
ticks: {
color: '#64748b'
},
grid: {
display: false
}
}
}
}
});
}
document.addEventListener('DOMContentLoaded', async function () {
const token = getAccessToken();
const role = getUserRole();
if (!token) {
window.location.href = routeLogin;
return;
}
if (role !== 'siswa') {
redirectByRole(role);
return;
}
const savedName = localStorage.getItem('user_name') || localStorage.getItem('name') || 'Siswa';
document.getElementById('student-name-label').innerText = savedName;
try {
const response = await fetch('/api/student/dashboard-stats', {
headers: {
@ -189,66 +707,57 @@
}
});
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');
}
if (response.status === 401) {
localStorage.removeItem('access_token');
localStorage.removeItem('user_role');
window.location.href = routeLogin;
return;
}
const result = await response.json();
if (!result.success) {
document.getElementById('stat-total').innerText = 0;
document.getElementById('stat-last').innerText = 'Belum ada rekomendasi';
renderActivityList([]);
renderTrendChart([]);
return;
}
const data = result.data || {};
const activities = Array.isArray(data.aktivitas_terbaru) ? data.aktivitas_terbaru : [];
const firstActivity = activities.length > 0 ? activities[0] : null;
document.getElementById('stat-total').innerText = data.total_aktivitas ?? 0;
const lastName = data.rekomendasi_terakhir || '';
document.getElementById('stat-last').innerText = isValidRecommendationName(lastName)
? lastName
: 'Belum ada rekomendasi';
// Simpan hasil terakhir dari data dashboard jika API mengirim id riwayat
const firstId = getHistoryId(firstActivity);
if (isValidRecommendationName(lastName) && firstId) {
saveStoredLastResult({
id: firstId,
jurusan: lastName,
tanggal: firstActivity?.tanggal || '',
url: makeResultUrl(firstId)
});
}
renderActivityList(activities);
renderTrendChart(data.tren_nilai || []);
} catch (error) {
console.error('Gagal memuat data dashboard:', error);
document.getElementById('stat-total').innerText = 0;
document.getElementById('stat-last').innerText = 'Data belum dapat dimuat';
renderActivityList([]);
renderTrendChart([]);
}
});
</script>

View File

@ -3,42 +3,146 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hasil Analisis - ASGARD</title>
<title>Hasil Rekomendasi - ASGARD</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Plus Jakarta Sans', sans-serif;
background-color: #f8fafc;
background:
radial-gradient(circle at 8% 12%, rgba(16, 185, 129, 0.10), transparent 32%),
radial-gradient(circle at 92% 8%, rgba(59, 130, 246, 0.08), transparent 30%),
linear-gradient(180deg, #f8fafc 0%, #f3f7fb 50%, #f8fafc 100%);
}
nav {
box-shadow: 0 10px 35px rgba(15, 23, 42, 0.05);
}
nav a {
position: relative;
}
nav a::after {
content: "";
position: absolute;
left: 0;
bottom: -10px;
width: 0;
height: 2px;
border-radius: 999px;
background: #10b981;
transition: width 0.25s ease;
}
nav a:hover::after,
nav a.text-emerald-600::after {
width: 100%;
}
.card-shadow {
box-shadow: 0 14px 40px rgba(15, 23, 42, 0.08);
box-shadow: 0 18px 50px rgba(15, 23, 42, 0.08);
}
.card-hover {
transition: transform 0.25s ease, box-shadow 0.25s ease, border-color 0.25s ease, background-color 0.25s ease;
}
.card-hover:hover {
transform: translateY(-4px);
box-shadow: 0 24px 60px rgba(15, 23, 42, 0.11);
border-color: rgba(16, 185, 129, 0.22);
}
main > div > .text-center {
background: rgba(255, 255, 255, 0.78);
border: 1px solid rgba(226, 232, 240, 0.85);
border-radius: 32px;
padding: 34px 24px;
box-shadow: 0 18px 55px rgba(15, 23, 42, 0.055);
backdrop-filter: blur(16px);
}
main section.bg-gradient-to-br {
box-shadow: 0 28px 75px rgba(16, 185, 129, 0.23);
}
main section.bg-white,
main div.bg-white {
background: rgba(255, 255, 255, 0.94);
backdrop-filter: blur(14px);
}
main .bg-slate-50 {
transition: transform 0.22s ease, box-shadow 0.22s ease, border-color 0.22s ease;
}
main .bg-slate-50:hover {
transform: translateY(-2px);
box-shadow: 0 16px 35px rgba(15, 23, 42, 0.06);
border-color: rgba(16, 185, 129, 0.18);
}
footer {
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
}
@media (max-width: 768px) {
main > div > .text-center {
border-radius: 26px;
padding: 26px 18px;
}
}
</style>
</head>
<body class="flex flex-col min-h-screen text-slate-800">
<!-- Navbar -->
<nav class="sticky top-0 z-50 bg-white/95 backdrop-blur-md border-b border-slate-100 px-6 py-4">
<div class="max-w-7xl mx-auto flex items-center justify-between gap-4">
<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 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="hidden md:flex items-center gap-8 text-[11px] font-bold uppercase tracking-widest text-slate-400">
<a href="{{ route('student.profile') }}" class="hover:text-emerald-600 transition">Beranda</a>
<a href="{{ route('student.prediction') }}" class="text-emerald-600">Cek Rekomendasi</a>
<a href="#" class="hover:text-emerald-600 transition">Kontak BK</a>
<a href="{{ route('student.profile') }}" class="hover:text-emerald-600 transition">
Beranda
</a>
<a href="{{ route('student.prediction') }}" class="hover:text-emerald-600 transition">
Cek Rekomendasi
</a>
<!-- Menu aktif halaman hasil -->
<a href="#" onclick="goToLastResult(event)" class="text-emerald-600">
Hasil Rekomendasi
</a>
<!-- <a href="#" class="hover:text-emerald-600 transition">
Kontak BK
</a> -->
</div>
<div class="flex items-center gap-3">
<div class="hidden sm: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">Siswa Terdaftar</span>
<span class="text-[10px] font-bold text-emerald-700 uppercase">
Siswa
</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>
<span class="text-[10px] font-bold uppercase tracking-widest">
Logout
</span>
</button>
</div>
</div>
@ -46,19 +150,24 @@
<main class="flex-grow px-6 py-10">
<div class="max-w-5xl mx-auto">
<!-- Header -->
<div class="text-center mb-8">
<div class="inline-flex items-center gap-2 bg-emerald-100 text-emerald-700 px-4 py-2 rounded-full text-xs font-bold">
Analisis Random Forest Selesai
Hasil rekomendasi berhasil diproses
</div>
<h1 class="mt-5 text-4xl md:text-5xl font-extrabold tracking-tight text-slate-900">
Laporan Rekomendasi AI
Hasil Rekomendasi Jurusan
</h1>
<p class="text-slate-500 mt-3">
Berdasarkan data akademik dan minat bakat, berikut diagnosis masa depanmu.
<p class="text-slate-500 mt-3 max-w-2xl mx-auto leading-relaxed">
Hasil ini tersimpan sebagai rekomendasi terakhir. Setelah kembali ke dashboard, menu <b>Hasil Rekomendasi</b> tetap bisa digunakan untuk membuka halaman ini.
</p>
</div>
<div class="bg-gradient-to-br from-emerald-600 via-emerald-500 to-teal-600 rounded-[2.5rem] p-8 md:p-14 text-white card-shadow mb-8 relative overflow-hidden">
<!-- Hasil utama -->
<section class="bg-gradient-to-br from-emerald-600 via-emerald-500 to-teal-600 rounded-[2.5rem] p-8 md:p-14 text-white card-shadow mb-8 relative overflow-hidden">
<div class="absolute inset-0 opacity-10">
<div class="absolute -top-10 -left-10 w-40 h-40 bg-white rounded-full blur-3xl"></div>
<div class="absolute -bottom-10 -right-10 w-56 h-56 bg-white rounded-full blur-3xl"></div>
@ -66,38 +175,57 @@
<div class="relative z-10 text-center">
<p class="text-[11px] md:text-sm font-bold uppercase tracking-[0.35em] text-emerald-100 mb-5">
Sangat Cocok di Bidang
Rekomendasi Jurusan
</p>
<h2 class="text-3xl md:text-6xl font-black uppercase leading-tight tracking-tight mb-8">
{{ $riwayat->hasil_rekomendasi_jurusan }}
</h2>
<p class="max-w-3xl mx-auto text-emerald-50 leading-relaxed mb-8">
Jurusan ini diperoleh dari pengolahan data nilai rapor, kecenderungan minat, dan kondisi ekonomi yang telah dimasukkan.
</p>
<div class="flex flex-col sm:flex-row gap-3 justify-center">
<a href="{{ route('student.prediction') }}"
class="inline-flex items-center justify-center bg-emerald-700/70 hover:bg-emerald-800 text-white font-bold py-4 px-8 rounded-2xl transition border border-white/10">
Ulangi Analisis
<!-- Tombol kembali, hasil tetap tersimpan di localStorage -->
<a href="{{ route('student.profile') }}"
onclick="saveLastRecommendationResult()"
class="inline-flex items-center justify-center bg-white hover:bg-slate-100 text-emerald-700 font-bold py-4 px-8 rounded-2xl transition">
Kembali ke Dashboard
</a>
<a href="{{ route('student.profile') }}"
class="inline-flex items-center justify-center bg-white hover:bg-slate-100 text-emerald-700 font-bold py-4 px-8 rounded-2xl transition">
Kembali ke Dashboard
<a href="{{ route('student.prediction') }}"
onclick="saveLastRecommendationResult()"
class="inline-flex items-center justify-center bg-emerald-800/50 hover:bg-emerald-800 text-white font-bold py-4 px-8 rounded-2xl transition border border-white/10">
Cek Rekomendasi Baru
</a>
</div>
</div>
</div>
</section>
<div class="bg-white rounded-[2rem] border border-slate-100 card-shadow p-6 md:p-8 mb-8">
<!-- Rekapan data -->
<section class="bg-white rounded-[2rem] border border-slate-100 card-shadow p-6 md:p-8 mb-8">
<div class="flex items-center gap-3 mb-6">
<div class="w-10 h-10 rounded-2xl bg-violet-100 flex items-center justify-center text-xl">👤</div>
<h3 class="text-2xl font-extrabold text-slate-800">Rekapan Profilmu</h3>
<div class="w-10 h-10 rounded-2xl bg-violet-100 flex items-center justify-center text-xl">
👤
</div>
<h3 class="text-2xl font-extrabold text-slate-800">
Rekapan Data yang Digunakan
</h3>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="bg-slate-50 border border-slate-100 rounded-3xl p-5">
<p class="text-[11px] font-bold uppercase tracking-widest text-slate-400 mb-3">Kondisi Ekonomi</p>
<div class="bg-slate-50 border border-slate-100 rounded-3xl p-5 card-hover">
<p class="text-[11px] font-bold uppercase tracking-widest text-slate-400 mb-3">
Kondisi Ekonomi
</p>
<div class="flex items-center gap-4">
<div class="w-14 h-14 rounded-2xl bg-amber-100 flex items-center justify-center text-2xl">💰</div>
<div class="w-14 h-14 rounded-2xl bg-amber-100 flex items-center justify-center text-2xl">
💰
</div>
<div>
<p class="font-extrabold text-slate-800 leading-snug">
{{ $labelEkonomi }}
@ -106,10 +234,16 @@ class="inline-flex items-center justify-center bg-white hover:bg-slate-100 text-
</div>
</div>
<div class="bg-slate-50 border border-slate-100 rounded-3xl p-5">
<p class="text-[11px] font-bold uppercase tracking-widest text-slate-400 mb-3">Minat Utama</p>
<div class="bg-slate-50 border border-slate-100 rounded-3xl p-5 card-hover">
<p class="text-[11px] font-bold uppercase tracking-widest text-slate-400 mb-3">
Minat Utama
</p>
<div class="flex items-center gap-4">
<div class="w-14 h-14 rounded-2xl bg-pink-100 flex items-center justify-center text-2xl">🎯</div>
<div class="w-14 h-14 rounded-2xl bg-pink-100 flex items-center justify-center text-2xl">
🎯
</div>
<div>
<p class="font-extrabold text-slate-800 leading-snug">
{{ $minatUtama }}
@ -118,10 +252,16 @@ class="inline-flex items-center justify-center bg-white hover:bg-slate-100 text-
</div>
</div>
<div class="bg-slate-50 border border-slate-100 rounded-3xl p-5">
<p class="text-[11px] font-bold uppercase tracking-widest text-slate-400 mb-3">Akademik</p>
<div class="bg-slate-50 border border-slate-100 rounded-3xl p-5 card-hover">
<p class="text-[11px] font-bold uppercase tracking-widest text-slate-400 mb-3">
Data Akademik
</p>
<div class="flex items-center gap-4">
<div class="w-14 h-14 rounded-2xl bg-cyan-100 flex items-center justify-center text-2xl">📚</div>
<div class="w-14 h-14 rounded-2xl bg-cyan-100 flex items-center justify-center text-2xl">
📚
</div>
<div>
<p class="font-extrabold text-slate-800 leading-snug">
15 Mata Pelajaran
@ -130,48 +270,111 @@ class="inline-flex items-center justify-center bg-white hover:bg-slate-100 text-
</div>
</div>
</div>
</div>
</section>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Informasi hasil -->
<section class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
<div class="bg-white rounded-[2rem] border border-slate-100 card-shadow p-6 md:p-8">
<h3 class="text-xl font-extrabold text-slate-800 mb-4">Ringkasan Analisis</h3>
<h3 class="text-xl font-extrabold text-slate-800 mb-4">
Ringkasan Analisis
</h3>
<p class="text-slate-600 leading-relaxed">
Berdasarkan perpaduan profil akademik, kecenderungan minat bakat, dan kondisi ekonomi keluarga,
sistem menilai bahwa jurusan ini merupakan pilihan yang paling relevan untuk dipertimbangkan pada tahap saat ini.
Sistem membaca hubungan antara nilai rapor, minat utama, dan kondisi ekonomi yang diinputkan. Berdasarkan hasil pengolahan tersebut, jurusan
<span class="font-bold text-slate-800">{{ $riwayat->hasil_rekomendasi_jurusan }}</span>
menjadi pilihan yang paling sesuai untuk dipertimbangkan pada tahap ini.
</p>
</div>
<div class="bg-white rounded-[2rem] border border-slate-100 card-shadow p-6 md:p-8">
<h3 class="text-xl font-extrabold text-slate-800 mb-4">Peluang Karier</h3>
<h3 class="text-xl font-extrabold text-slate-800 mb-4">
Peluang Karier
</h3>
<p class="text-slate-600 leading-relaxed">
{{ $prospekKerja }}
</p>
</div>
</div>
</section>
<!-- Saran -->
<section class="bg-white rounded-[2rem] border border-slate-100 card-shadow p-6 md:p-8">
<h3 class="text-xl font-extrabold text-slate-800 mb-5">
Langkah Setelah Mendapatkan Hasil
</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="bg-slate-50 border border-slate-100 rounded-3xl p-5">
<h4 class="font-extrabold text-slate-800 mb-2">
1. Cek Kembali Data
</h4>
<p class="text-sm text-slate-500 leading-relaxed">
Pastikan nilai dan jawaban minat yang dimasukkan sudah sesuai dengan kondisi sebenarnya.
</p>
</div>
<div class="bg-slate-50 border border-slate-100 rounded-3xl p-5">
<h4 class="font-extrabold text-slate-800 mb-2">
2. Cari Informasi Jurusan
</h4>
<p class="text-sm text-slate-500 leading-relaxed">
Pelajari mata kuliah, biaya, peluang kerja, dan kampus yang menyediakan jurusan tersebut.
</p>
</div>
<div class="bg-slate-50 border border-slate-100 rounded-3xl p-5">
<h4 class="font-extrabold text-slate-800 mb-2">
3. Konsultasi Guru BK
</h4>
<p class="text-sm text-slate-500 leading-relaxed">
Gunakan hasil ini sebagai bahan diskusi dengan Guru BK sebelum menentukan pilihan akhir.
</p>
</div>
</div>
</section>
</div>
</main>
<!-- Footer -->
<footer class="bg-[#062C22] text-white py-16 px-6 mt-16">
<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>
<span class="font-black text-2xl tracking-tighter uppercase italic">
ASGARD.
</span>
<p class="text-slate-300 text-sm leading-relaxed max-w-xs">
Sistem pakar rekomendasi jurusan kuliah SMAN 4 Jember berbasis data-driven.
Sistem rekomendasi jurusan kuliah SMAN 4 Jember berbasis profil siswa dan data akademik.
</p>
</div>
<div>
<h5 class="font-bold text-xs uppercase tracking-widest text-emerald-400 mb-6">Informasi</h5>
<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>
<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>
<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>
@ -181,13 +384,18 @@ class="inline-flex items-center justify-center bg-white hover:bg-slate-100 text-
</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">
<p class="text-[10px] font-medium uppercase tracking-[0.35em] text-slate-400">
© 2026 GALANG SEFIAN ADJI - POLITEKNIK NEGERI JEMBER
</p>
</div>
</footer>
<script>
const routeLogin = @json(route('login'));
const routeDashboard = @json(route('student.profile'));
const routePrediction = @json(route('student.prediction'));
const routeAdminDashboard = @json(route('admin.dashboard'));
function getAccessToken() {
return localStorage.getItem('access_token');
}
@ -198,16 +406,70 @@ function getUserRole() {
function redirectByRole(role) {
if (role === 'admin') {
window.location.href = "{{ route('admin.dashboard') }}";
window.location.href = routeAdminDashboard;
return;
}
if (role === 'siswa') {
window.location.href = "{{ route('student.profile') }}";
window.location.href = routeDashboard;
return;
}
window.location.href = "{{ route('login') }}";
window.location.href = routeLogin;
}
function getStoredLastResult() {
try {
const saved = localStorage.getItem('asgard_last_result');
return saved ? JSON.parse(saved) : null;
} catch (error) {
return null;
}
}
function goToLastResult(event) {
event.preventDefault();
saveLastRecommendationResult();
const saved = getStoredLastResult();
if (saved && saved.url) {
window.location.href = saved.url;
return;
}
if (saved && saved.id) {
window.location.href = `/student/result/${saved.id}`;
return;
}
alert('Belum ada hasil rekomendasi yang bisa dibuka. Silakan lakukan cek rekomendasi terlebih dahulu.');
window.location.href = routePrediction;
}
// Bagian penting: menyimpan hasil terakhir agar setelah klik kembali tetap bisa dibuka dari navbar
function saveLastRecommendationResult() {
const resultId = @json($riwayat->id ?? '');
const resultUrl = resultId ? `/student/result/${resultId}` : window.location.pathname;
const resultData = {
id: resultId,
jurusan: @json($riwayat->hasil_rekomendasi_jurusan ?? ''),
label_ekonomi: @json($labelEkonomi ?? ''),
minat_utama: @json($minatUtama ?? ''),
prospek_kerja: @json($prospekKerja ?? ''),
url: resultUrl,
tanggal: new Date().toLocaleString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
})
};
localStorage.setItem('asgard_last_result', JSON.stringify(resultData));
}
async function handleLogout() {
@ -229,21 +491,30 @@ function redirectByRole(role) {
localStorage.removeItem('access_token');
localStorage.removeItem('user_role');
window.location.href = "{{ route('login') }}";
localStorage.removeItem('asgard_last_result');
window.location.href = routeLogin;
}
// bfcache/pageshow membuat hasil tetap tersimpan walaupun user memakai tombol back browser
window.addEventListener('pageshow', function () {
saveLastRecommendationResult();
});
document.addEventListener('DOMContentLoaded', function () {
const token = getAccessToken();
const role = getUserRole();
if (!token) {
window.location.href = "{{ route('login') }}";
window.location.href = routeLogin;
return;
}
if (role !== 'siswa') {
redirectByRole(role);
return;
}
saveLastRecommendationResult();
});
</script>
</body>

View File

@ -4,42 +4,136 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cek Rekomendasi - ASGARD</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;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;
background:
radial-gradient(circle at 8% 12%, rgba(16, 185, 129, 0.10), transparent 32%),
radial-gradient(circle at 92% 8%, rgba(59, 130, 246, 0.08), transparent 30%),
linear-gradient(180deg, #f8fafc 0%, #f3f7fb 50%, #f8fafc 100%);
}
nav {
box-shadow: 0 10px 35px rgba(15, 23, 42, 0.05);
}
nav a {
position: relative;
}
nav a::after {
content: "";
position: absolute;
left: 0;
bottom: -10px;
width: 0;
height: 2px;
border-radius: 999px;
background: #10b981;
transition: width 0.25s ease;
}
nav a:hover::after,
nav a.text-emerald-600::after {
width: 100%;
}
.card-shadow {
box-shadow: 0 14px 40px rgba(15, 23, 42, 0.08);
box-shadow: 0 18px 50px rgba(15, 23, 42, 0.08);
}
main > div > div.bg-white {
background: rgba(255, 255, 255, 0.94);
backdrop-filter: blur(16px);
box-shadow: 0 24px 70px rgba(15, 23, 42, 0.08);
}
main section {
transition: transform 0.22s ease, box-shadow 0.22s ease, border-color 0.22s ease;
}
main section:hover {
transform: translateY(-2px);
box-shadow: 0 16px 35px rgba(15, 23, 42, 0.055);
border-color: rgba(16, 185, 129, 0.18);
}
input,
select {
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease, background-color 0.2s ease;
}
input:hover,
select:hover {
border-color: rgba(16, 185, 129, 0.45);
background-color: #ffffff;
}
input:focus,
select:focus {
transform: translateY(-1px);
}
#alert-box {
box-shadow: 0 14px 35px rgba(239, 68, 68, 0.08);
}
footer {
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
}
</style>
</head>
<body class="flex flex-col min-h-screen text-slate-800">
<!-- Navbar -->
<nav class="sticky top-0 z-50 bg-white/95 backdrop-blur-md border-b border-slate-100 px-6 py-4">
<div class="max-w-7xl mx-auto flex items-center justify-between gap-4">
<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 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="hidden md:flex items-center gap-8 text-[11px] font-bold uppercase tracking-widest text-slate-400">
<a href="{{ route('student.profile') }}" class="hover:text-emerald-600 transition">Beranda</a>
<a href="{{ route('student.prediction') }}" class="text-emerald-600">Cek Rekomendasi</a>
<a href="#" class="hover:text-emerald-600 transition">Kontak BK</a>
<a href="{{ route('student.profile') }}" class="hover:text-emerald-600 transition">
Beranda
</a>
<a href="{{ route('student.prediction') }}" class="text-emerald-600">
Cek Rekomendasi
</a>
<!-- Menu untuk membuka hasil terakhir -->
<a href="#" onclick="goToLastResult(event)" class="hover:text-emerald-600 transition">
Hasil Rekomendasi
</a>
<!-- <a href="#" class="hover:text-emerald-600 transition">
Kontak BK
</a> -->
</div>
<div class="flex items-center gap-3">
<div class="hidden sm: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">Siswa Terdaftar</span>
<span class="text-[10px] font-bold text-emerald-700 uppercase">
Siswa
</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>
<span class="text-[10px] font-bold uppercase tracking-widest">
Logout
</span>
</button>
</div>
</div>
@ -48,21 +142,31 @@
<main class="flex-grow px-6 py-10">
<div class="max-w-3xl mx-auto">
<div class="bg-white rounded-[2rem] card-shadow border border-slate-100 px-6 md:px-10 py-8 md:py-10">
<div class="text-center mb-8">
<div class="w-12 h-12 mx-auto rounded-2xl bg-emerald-100 flex items-center justify-center text-2xl mb-4">
🧠
📝
</div>
<h1 class="text-3xl md:text-4xl font-extrabold tracking-tight text-slate-900">DIAGNOSIS JURUSAN</h1>
<p class="text-slate-500 mt-3 text-sm md:text-base">
Isi data berikut dengan jujur agar sistem dapat memberikan rekomendasi jurusan kuliah yang sesuai.
<h1 class="text-3xl md:text-4xl font-extrabold tracking-tight text-slate-900">
Form Rekomendasi Jurusan
</h1>
<p class="text-slate-500 mt-3 text-sm md:text-base leading-relaxed">
Isi data berikut jika ingin membuat rekomendasi baru. Jika ingin melihat hasil sebelumnya, klik menu <b>Hasil Rekomendasi</b> pada navbar.
</p>
</div>
<div id="alert-box" class="hidden mb-6 rounded-2xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700"></div>
<div class="space-y-6">
<!-- Kondisi ekonomi -->
<section class="bg-slate-50 border border-slate-100 rounded-3xl p-5 md:p-6">
<h2 class="font-extrabold text-slate-800 mb-1">💰 Kondisi Ekonomi</h2>
<h2 class="font-extrabold text-slate-800 mb-1">
💰 Kondisi Ekonomi
</h2>
<p class="text-sm text-slate-500 mb-4">
Masukkan estimasi pendapatan gabungan orang tua per bulan dalam rupiah.
</p>
@ -80,14 +184,18 @@ class="w-full rounded-2xl border border-slate-200 bg-white px-4 py-4 text-base f
</p>
</section>
<!-- Minat dan bakat -->
<section class="bg-emerald-50/40 border border-emerald-100 rounded-3xl p-5 md:p-6">
<h2 class="font-extrabold text-emerald-800 mb-4">🎯 Kuesioner Minat & Bakat</h2>
<h2 class="font-extrabold text-emerald-800 mb-4">
🎯 Kuesioner Minat dan Bakat
</h2>
<div class="space-y-4">
<div>
<label for="q1" class="block text-sm font-semibold text-slate-700 mb-2">
1. Aktivitas apa yang paling kamu sukai?
</label>
<select id="q1" class="question-select w-full rounded-2xl border border-slate-200 bg-white px-4 py-4 text-sm text-slate-700 outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
<option value="">Pilih jawaban</option>
<option value="r">Memperbaiki alat, praktik lapangan, atau pekerjaan teknis</option>
@ -103,6 +211,7 @@ class="w-full rounded-2xl border border-slate-200 bg-white px-4 py-4 text-base f
<label for="q2" class="block text-sm font-semibold text-slate-700 mb-2">
2. Saat ada tugas kelompok, kamu paling nyaman berperan sebagai apa?
</label>
<select id="q2" class="question-select w-full rounded-2xl border border-slate-200 bg-white px-4 py-4 text-sm text-slate-700 outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
<option value="">Pilih jawaban</option>
<option value="r">Pelaksana teknis atau praktik</option>
@ -118,6 +227,7 @@ class="w-full rounded-2xl border border-slate-200 bg-white px-4 py-4 text-base f
<label for="q3" class="block text-sm font-semibold text-slate-700 mb-2">
3. Jenis pekerjaan masa depan yang paling menarik buatmu?
</label>
<select id="q3" class="question-select w-full rounded-2xl border border-slate-200 bg-white px-4 py-4 text-sm text-slate-700 outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
<option value="">Pilih jawaban</option>
<option value="r">Teknisi, operator, atau pekerjaan lapangan</option>
@ -133,6 +243,7 @@ class="w-full rounded-2xl border border-slate-200 bg-white px-4 py-4 text-base f
<label for="q4" class="block text-sm font-semibold text-slate-700 mb-2">
4. Kegiatan sekolah seperti apa yang paling kamu nikmati?
</label>
<select id="q4" class="question-select w-full rounded-2xl border border-slate-200 bg-white px-4 py-4 text-sm text-slate-700 outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
<option value="">Pilih jawaban</option>
<option value="r">Praktikum, keterampilan, atau kegiatan teknis</option>
@ -148,6 +259,7 @@ class="w-full rounded-2xl border border-slate-200 bg-white px-4 py-4 text-base f
<label for="q5" class="block text-sm font-semibold text-slate-700 mb-2">
5. Kamu paling nyaman bekerja dalam suasana seperti apa?
</label>
<select id="q5" class="question-select w-full rounded-2xl border border-slate-200 bg-white px-4 py-4 text-sm text-slate-700 outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
<option value="">Pilih jawaban</option>
<option value="r">Aktif, praktik, dan langsung di lapangan</option>
@ -161,8 +273,12 @@ class="w-full rounded-2xl border border-slate-200 bg-white px-4 py-4 text-base f
</div>
</section>
<!-- Nilai rapor -->
<section class="bg-slate-50 border border-slate-100 rounded-3xl p-5 md:p-6">
<h2 class="font-extrabold text-slate-700 mb-2">📚 Nilai Rapor</h2>
<h2 class="font-extrabold text-slate-700 mb-2">
📚 Nilai Rapor
</h2>
<p class="text-sm text-slate-500 mb-4">
Masukkan nilai rapor pada setiap mata pelajaran dengan rentang 0 sampai 100.
</p>
@ -172,18 +288,22 @@ class="w-full rounded-2xl border border-slate-200 bg-white px-4 py-4 text-base f
<label class="block text-xs font-bold text-slate-500 mb-2">Agama</label>
<input type="number" id="agama" min="0" max="100" placeholder="0 - 100" class="w-full rounded-xl border border-slate-200 bg-white px-3 py-3 font-semibold outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
</div>
<div>
<label class="block text-xs font-bold text-slate-500 mb-2">PKN</label>
<input type="number" id="pkn" min="0" max="100" placeholder="0 - 100" class="w-full rounded-xl border border-slate-200 bg-white px-3 py-3 font-semibold outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
</div>
<div>
<label class="block text-xs font-bold text-slate-500 mb-2">B. Indonesia</label>
<input type="number" id="bahasa_indonesia" min="0" max="100" placeholder="0 - 100" class="w-full rounded-xl border border-slate-200 bg-white px-3 py-3 font-semibold outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
</div>
<div>
<label class="block text-xs font-bold text-slate-500 mb-2">B. Inggris</label>
<input type="number" id="bahasa_inggris" min="0" max="100" placeholder="0 - 100" class="w-full rounded-xl border border-slate-200 bg-white px-3 py-3 font-semibold outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
</div>
<div>
<label class="block text-xs font-bold text-slate-500 mb-2">Matematika</label>
<input type="number" id="matematika" min="0" max="100" placeholder="0 - 100" class="w-full rounded-xl border border-slate-200 bg-white px-3 py-3 font-semibold outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
@ -193,18 +313,22 @@ class="w-full rounded-2xl border border-slate-200 bg-white px-4 py-4 text-base f
<label class="block text-xs font-bold text-slate-500 mb-2">Fisika</label>
<input type="number" id="fisika" min="0" max="100" placeholder="0 - 100" class="w-full rounded-xl border border-slate-200 bg-white px-3 py-3 font-semibold outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
</div>
<div>
<label class="block text-xs font-bold text-slate-500 mb-2">Kimia</label>
<input type="number" id="kimia" min="0" max="100" placeholder="0 - 100" class="w-full rounded-xl border border-slate-200 bg-white px-3 py-3 font-semibold outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
</div>
<div>
<label class="block text-xs font-bold text-slate-500 mb-2">Biologi</label>
<input type="number" id="biologi" min="0" max="100" placeholder="0 - 100" class="w-full rounded-xl border border-slate-200 bg-white px-3 py-3 font-semibold outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
</div>
<div>
<label class="block text-xs font-bold text-slate-500 mb-2">Sejarah</label>
<input type="number" id="sejarah" min="0" max="100" placeholder="0 - 100" class="w-full rounded-xl border border-slate-200 bg-white px-3 py-3 font-semibold outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
</div>
<div>
<label class="block text-xs font-bold text-slate-500 mb-2">Geografi</label>
<input type="number" id="geografi" min="0" max="100" placeholder="0 - 100" class="w-full rounded-xl border border-slate-200 bg-white px-3 py-3 font-semibold outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
@ -214,18 +338,22 @@ class="w-full rounded-2xl border border-slate-200 bg-white px-4 py-4 text-base f
<label class="block text-xs font-bold text-slate-500 mb-2">Ekonomi</label>
<input type="number" id="ekonomi_mapel" min="0" max="100" placeholder="0 - 100" class="w-full rounded-xl border border-slate-200 bg-white px-3 py-3 font-semibold outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
</div>
<div>
<label class="block text-xs font-bold text-slate-500 mb-2">Sosiologi</label>
<input type="number" id="sosiologi" min="0" max="100" placeholder="0 - 100" class="w-full rounded-xl border border-slate-200 bg-white px-3 py-3 font-semibold outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
</div>
<div>
<label class="block text-xs font-bold text-slate-500 mb-2">Seni Budaya</label>
<input type="number" id="seni_budaya" min="0" max="100" placeholder="0 - 100" class="w-full rounded-xl border border-slate-200 bg-white px-3 py-3 font-semibold outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
</div>
<div>
<label class="block text-xs font-bold text-slate-500 mb-2">PJOK</label>
<input type="number" id="pjok" min="0" max="100" placeholder="0 - 100" class="w-full rounded-xl border border-slate-200 bg-white px-3 py-3 font-semibold outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
</div>
<div>
<label class="block text-xs font-bold text-slate-500 mb-2">Informatika</label>
<input type="number" id="informatika" min="0" max="100" placeholder="0 - 100" class="w-full rounded-xl border border-slate-200 bg-white px-3 py-3 font-semibold outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
@ -233,10 +361,11 @@ class="w-full rounded-2xl border border-slate-200 bg-white px-4 py-4 text-base f
</div>
</section>
<!-- Tombol -->
<div class="flex flex-col sm:flex-row gap-3 pt-2">
<a href="{{ route('student.profile') }}"
class="w-full sm:w-1/3 text-center bg-slate-100 hover:bg-slate-200 text-slate-600 font-bold py-4 rounded-2xl transition">
Batal
Kembali
</a>
<button id="btn-submit"
@ -249,27 +378,43 @@ class="w-full sm:w-2/3 bg-emerald-600 hover:bg-emerald-700 text-white font-bold
</div>
</main>
<!-- Footer -->
<footer class="bg-[#062C22] text-white py-16 px-6 mt-16">
<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>
<span class="font-black text-2xl tracking-tighter uppercase italic">
ASGARD.
</span>
<p class="text-slate-300 text-sm leading-relaxed max-w-xs">
Sistem pakar rekomendasi jurusan kuliah SMAN 4 Jember berbasis data-driven.
Sistem rekomendasi jurusan kuliah SMAN 4 Jember berbasis profil siswa dan data akademik.
</p>
</div>
<div>
<h5 class="font-bold text-xs uppercase tracking-widest text-emerald-400 mb-6">Informasi</h5>
<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>
<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>
<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>
@ -279,7 +424,7 @@ class="w-full sm:w-2/3 bg-emerald-600 hover:bg-emerald-700 text-white font-bold
</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">
<p class="text-[10px] font-medium uppercase tracking-[0.35em] text-slate-400">
© 2026 GALANG SEFIAN ADJI - POLITEKNIK NEGERI JEMBER
</p>
</div>
@ -289,10 +434,19 @@ class="w-full sm:w-2/3 bg-emerald-600 hover:bg-emerald-700 text-white font-bold
const alertBox = document.getElementById('alert-box');
const btnSubmit = document.getElementById('btn-submit');
const routeLogin = @json(route('login'));
const routeDashboard = @json(route('student.profile'));
const routePrediction = @json(route('student.prediction'));
const routeAdminDashboard = @json(route('admin.dashboard'));
function showError(message) {
alertBox.classList.remove('hidden');
alertBox.textContent = message;
window.scrollTo({ top: 0, behavior: 'smooth' });
window.scrollTo({
top: 0,
behavior: 'smooth'
});
}
function hideError() {
@ -314,16 +468,44 @@ function getUserRole() {
function redirectByRole(role) {
if (role === 'admin') {
window.location.href = "{{ route('admin.dashboard') }}";
window.location.href = routeAdminDashboard;
return;
}
if (role === 'siswa') {
window.location.href = "{{ route('student.profile') }}";
window.location.href = routeDashboard;
return;
}
window.location.href = "{{ route('login') }}";
window.location.href = routeLogin;
}
function getStoredLastResult() {
try {
const saved = localStorage.getItem('asgard_last_result');
return saved ? JSON.parse(saved) : null;
} catch (error) {
return null;
}
}
function goToLastResult(event) {
event.preventDefault();
const saved = getStoredLastResult();
if (saved && saved.url) {
window.location.href = saved.url;
return;
}
if (saved && saved.id) {
window.location.href = `/student/result/${saved.id}`;
return;
}
alert('Belum ada hasil rekomendasi yang bisa dibuka. Silakan lakukan cek rekomendasi terlebih dahulu.');
window.location.href = routePrediction;
}
function validateForm(payload) {
@ -341,6 +523,7 @@ function validateForm(payload) {
}
const value = Number(payload[field]);
if (isNaN(value) || value < 0 || value > 100) {
return `Nilai ${field.replaceAll('_', ' ')} harus berada pada rentang 0 sampai 100.`;
}
@ -355,6 +538,7 @@ function validateForm(payload) {
}
const questionIds = ['q1', 'q2', 'q3', 'q4', 'q5'];
for (const id of questionIds) {
if (!document.getElementById(id).value) {
return 'Semua pertanyaan minat dan bakat wajib diisi.';
@ -365,11 +549,20 @@ function validateForm(payload) {
}
function calculateRiasecScores() {
const scores = { r: 0, i: 0, a: 0, s: 0, e: 0, c: 0 };
const scores = {
r: 0,
i: 0,
a: 0,
s: 0,
e: 0,
c: 0
};
const questionIds = ['q1', 'q2', 'q3', 'q4', 'q5'];
questionIds.forEach(id => {
const value = document.getElementById(id).value;
if (value && scores.hasOwnProperty(value)) {
scores[value] += 20;
}
@ -397,7 +590,8 @@ function calculateRiasecScores() {
localStorage.removeItem('access_token');
localStorage.removeItem('user_role');
window.location.href = "{{ route('login') }}";
localStorage.removeItem('asgard_last_result');
window.location.href = routeLogin;
}
document.addEventListener('DOMContentLoaded', function () {
@ -405,12 +599,13 @@ function calculateRiasecScores() {
const role = getUserRole();
if (!token) {
window.location.href = "{{ route('login') }}";
window.location.href = routeLogin;
return;
}
if (role !== 'siswa') {
redirectByRole(role);
return;
}
});
@ -422,9 +617,11 @@ function calculateRiasecScores() {
if (!token) {
showError('Sesi login tidak ditemukan. Silakan login kembali.');
setTimeout(() => {
window.location.href = "{{ route('login') }}";
window.location.href = routeLogin;
}, 1200);
return;
}
@ -433,7 +630,7 @@ function calculateRiasecScores() {
return;
}
btnSubmit.innerHTML = 'Memeriksa Rekomendasi...';
btnSubmit.innerHTML = 'Memproses Rekomendasi...';
btnSubmit.disabled = true;
const scores = calculateRiasecScores();
@ -464,9 +661,10 @@ function calculateRiasecScores() {
};
const validationError = validateForm(payload);
if (validationError) {
showError(validationError);
btnSubmit.innerHTML = 'Cek Rekomendasi';
btnSubmit.innerHTML = 'Proses Rekomendasi';
btnSubmit.disabled = false;
return;
}
@ -496,8 +694,9 @@ function calculateRiasecScores() {
localStorage.removeItem('user_role');
setTimeout(() => {
window.location.href = "{{ route('login') }}";
window.location.href = routeLogin;
}, 1200);
return;
}
@ -507,10 +706,11 @@ function calculateRiasecScores() {
}
showError(result.message || 'Terjadi kesalahan saat memproses rekomendasi.');
} catch (error) {
showError('Kesalahan jaringan. Pastikan Laravel dan API Flask sedang berjalan.');
} finally {
btnSubmit.innerHTML = 'Cek Rekomendasi';
btnSubmit.innerHTML = 'Proses Rekomendasi';
btnSubmit.disabled = false;
}
});