push perubahan pertama kali
This commit is contained in:
parent
c85df91ce7
commit
40a227ca7a
|
|
@ -22,3 +22,17 @@
|
||||||
Homestead.json
|
Homestead.json
|
||||||
Homestead.yaml
|
Homestead.yaml
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
|
python_D/
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
.venv/
|
||||||
|
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
|
||||||
|
.ipynb_checkpoints/
|
||||||
|
|
||||||
|
*.swp
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
|
||||||
|
class AdminDashboardController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
return view('admin.dashboard');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
|
||||||
|
class AlumniController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
return view('admin.alumni.alumni');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
return view('admin.alumni.alumnicreate');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function edit($id)
|
||||||
|
{
|
||||||
|
return view('admin.alumni.alumniedit', compact('id'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
|
||||||
|
class GuruController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
return view('admin.guru.guru');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
return view('admin.guru.gurucreate');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function edit($id)
|
||||||
|
{
|
||||||
|
return view('admin.guru.guruedit', compact('id'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
|
||||||
|
class JurusanController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
return view('admin.jurusan.jurusan');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
return view('admin.jurusan.jurusancreate');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function edit($id)
|
||||||
|
{
|
||||||
|
return view('admin.jurusan.jurusanedit', compact('id'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class ProspekKerjaController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
return view('admin.prospek.prospekkerja');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
return view('admin.prospek.prospekkerjacreate');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function edit($id)
|
||||||
|
{
|
||||||
|
return view('admin.prospek.prospekkerjaedit', compact('id'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
|
||||||
|
class RiwayatController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
return view('admin.riwayat');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
|
||||||
|
class SiswaController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
return view('admin.siswa.siswa');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
return view('admin.siswa.siswacreate');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function edit($id)
|
||||||
|
{
|
||||||
|
return view('admin.siswa.siswaedit', compact('id'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Alumni;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
|
||||||
|
class AlumniController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$alumni = Alumni::all();
|
||||||
|
return response()->json(['success' => true, 'data' => $alumni], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$validator = Validator::make($request->all(), [
|
||||||
|
'nama_alumni' => 'required|string|max:255',
|
||||||
|
'perguruan_tinggi' => 'required|string',
|
||||||
|
'jurusan_diambil' => 'required|string',
|
||||||
|
], [
|
||||||
|
'nama_alumni.required' => 'Nama alumni wajib diisi.',
|
||||||
|
'perguruan_tinggi.required' => 'Nama Perguruan Tinggi harus diisi.',
|
||||||
|
'jurusan_diambil.required' => 'Jurusan yang diambil tidak boleh kosong.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$alumni = Alumni::create($request->all());
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Alumni ' . $alumni->id_alumni . ' berhasil ditambahkan.',
|
||||||
|
'data' => $alumni
|
||||||
|
], 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, $id)
|
||||||
|
{
|
||||||
|
$alumni = Alumni::find($id);
|
||||||
|
if (!$alumni) return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
|
||||||
|
|
||||||
|
$validator = Validator::make($request->all(), [
|
||||||
|
'nama_alumni' => 'required|string|max:255',
|
||||||
|
'perguruan_tinggi' => 'required|string',
|
||||||
|
'jurusan_diambil' => 'required|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$alumni->update($request->all());
|
||||||
|
return response()->json(['success' => true, 'message' => 'Data ' . $alumni->id_alumni . ' berhasil diperbarui'], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy($id)
|
||||||
|
{
|
||||||
|
$alumni = Alumni::find($id);
|
||||||
|
if (!$alumni) return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
|
||||||
|
$alumni->delete();
|
||||||
|
return response()->json(['success' => true, 'message' => 'Data Alumni berhasil dihapus'], 200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\RiwayatRekomendasi;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class DashboardAdminController extends Controller
|
||||||
|
{
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// Hitung siswa (fallback ke ID unik di tabel riwayat jika data role user kosong/berbeda)
|
||||||
|
$totalSiswa = User::whereIn('role', ['siswa', 'student'])->count();
|
||||||
|
if ($totalSiswa === 0) {
|
||||||
|
$totalSiswa = RiwayatRekomendasi::distinct('user_id')->count('user_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hitung total semua prediksi yang sudah dilakukan
|
||||||
|
$totalRekomendasi = RiwayatRekomendasi::count();
|
||||||
|
|
||||||
|
// Cek nilai rata-rata akurasi secara dinamis (default 86.4% jika kolom tidak ditemukan)
|
||||||
|
$rataAkurasi = 86.4;
|
||||||
|
try {
|
||||||
|
$tableName = (new RiwayatRekomendasi())->getTable();
|
||||||
|
if (Schema::hasColumn($tableName, 'probabilitas')) {
|
||||||
|
$avg = RiwayatRekomendasi::avg('probabilitas');
|
||||||
|
if ($avg) $rataAkurasi = round($avg, 2);
|
||||||
|
} elseif (Schema::hasColumn($tableName, 'akurasi')) {
|
||||||
|
$avg = RiwayatRekomendasi::avg('akurasi');
|
||||||
|
if ($avg) $rataAkurasi = round($avg, 2);
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {}
|
||||||
|
|
||||||
|
// Kelompokkan hasil rekomendasi untuk ditampilkan pada grafik
|
||||||
|
$distribusiJurusan = RiwayatRekomendasi::selectRaw('hasil_rekomendasi_jurusan as jurusan, count(*) as total')
|
||||||
|
->whereNotNull('hasil_rekomendasi_jurusan')
|
||||||
|
->groupBy('hasil_rekomendasi_jurusan')
|
||||||
|
->orderBy('total', 'desc')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => [
|
||||||
|
'total_siswa' => $totalSiswa,
|
||||||
|
'rekomendasi_diproses' => $totalRekomendasi,
|
||||||
|
'rata_rata_akurasi' => $rataAkurasi,
|
||||||
|
'distribusi_jurusan' => $distribusiJurusan
|
||||||
|
]
|
||||||
|
], 200);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Error: ' . $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Guru;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
|
||||||
|
class GuruController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$guru = Guru::all();
|
||||||
|
return response()->json(['success' => true, 'data' => $guru], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$validator = Validator::make($request->all(), [
|
||||||
|
'nama_guru' => 'required|string|max:255',
|
||||||
|
], [
|
||||||
|
'nama_guru.required' => 'Nama guru tidak boleh kosong.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$guru = Guru::create($request->all());
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Guru ' . $guru->id_guru . ' berhasil ditambahkan.',
|
||||||
|
'data' => $guru
|
||||||
|
], 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, $id)
|
||||||
|
{
|
||||||
|
$guru = Guru::find($id);
|
||||||
|
if (!$guru) return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
|
||||||
|
|
||||||
|
$validator = Validator::make($request->all(), [
|
||||||
|
'nama_guru' => 'required|string|max:255',
|
||||||
|
], [
|
||||||
|
'nama_guru.required' => 'Update gagal, nama guru harus diisi.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$guru->update($request->all());
|
||||||
|
return response()->json(['success' => true, 'message' => 'Data ' . $guru->id_guru . ' berhasil diperbarui'], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy($id)
|
||||||
|
{
|
||||||
|
$guru = Guru::find($id);
|
||||||
|
if (!$guru) return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
|
||||||
|
$guru->delete();
|
||||||
|
return response()->json(['success' => true, 'message' => 'Data Guru berhasil dihapus'], 200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Jurusan;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
|
||||||
|
class JurusanController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$jurusan = Jurusan::all();
|
||||||
|
return response()->json(['success' => true, 'data' => $jurusan], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$validator = Validator::make($request->all(), [
|
||||||
|
'nama_jurusan' => 'required|string|max:255',
|
||||||
|
'rumpun' => 'required|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$jurusan = Jurusan::create($request->all());
|
||||||
|
return response()->json(['success' => true, 'message' => 'Jurusan berhasil ditambahkan', 'data' => $jurusan], 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show($id)
|
||||||
|
{
|
||||||
|
$jurusan = Jurusan::find($id);
|
||||||
|
if (!$jurusan) return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
|
||||||
|
return response()->json(['success' => true, 'data' => $jurusan], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, $id)
|
||||||
|
{
|
||||||
|
$jurusan = Jurusan::find($id);
|
||||||
|
if (!$jurusan) return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
|
||||||
|
|
||||||
|
$jurusan->update($request->all());
|
||||||
|
return response()->json(['success' => true, 'message' => 'Jurusan berhasil diperbarui', 'data' => $jurusan], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy($id)
|
||||||
|
{
|
||||||
|
$jurusan = Jurusan::find($id);
|
||||||
|
if (!$jurusan) return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
|
||||||
|
|
||||||
|
$jurusan->delete();
|
||||||
|
return response()->json(['success' => true, 'message' => 'Jurusan berhasil dihapus'], 200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\ProspekKerja;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
|
||||||
|
class ProspekKerjaController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$prospek = ProspekKerja::with('jurusan')->orderBy('id', 'desc')->get();
|
||||||
|
return response()->json(['success' => true, 'data' => $prospek], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$validator = Validator::make($request->all(), [
|
||||||
|
'jurusan_id' => 'required|exists:jurusans,id',
|
||||||
|
'nama_profesi' => 'required|string|max:255',
|
||||||
|
'range_gaji' => 'nullable|string|max:255',
|
||||||
|
'deskripsi_pekerjaan' => 'nullable|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$prospek = ProspekKerja::create($request->all());
|
||||||
|
return response()->json(['success' => true, 'message' => 'Prospek Kerja berhasil ditambahkan', 'data' => $prospek], 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show($id)
|
||||||
|
{
|
||||||
|
$prospek = ProspekKerja::find($id);
|
||||||
|
if (!$prospek) return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
|
||||||
|
return response()->json(['success' => true, 'data' => $prospek], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, $id)
|
||||||
|
{
|
||||||
|
$prospek = ProspekKerja::find($id);
|
||||||
|
if (!$prospek) return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
|
||||||
|
|
||||||
|
$validator = Validator::make($request->all(), [
|
||||||
|
'jurusan_id' => 'required|exists:jurusans,id',
|
||||||
|
'nama_profesi' => 'required|string|max:255',
|
||||||
|
'range_gaji' => 'nullable|string|max:255',
|
||||||
|
'deskripsi_pekerjaan' => 'nullable|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$prospek->update($request->all());
|
||||||
|
return response()->json(['success' => true, 'message' => 'Prospek Kerja berhasil diperbarui', 'data' => $prospek], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy($id)
|
||||||
|
{
|
||||||
|
$prospek = ProspekKerja::find($id);
|
||||||
|
if (!$prospek) return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
|
||||||
|
|
||||||
|
$prospek->delete();
|
||||||
|
return response()->json(['success' => true, 'message' => 'Prospek Kerja berhasil dihapus'], 200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\RiwayatRekomendasi;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class RiwayatController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Mengambil semua data riwayat prediksi
|
||||||
|
*/
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$riwayat = RiwayatRekomendasi::with('user:id,name')
|
||||||
|
->orderBy('created_at', 'desc')
|
||||||
|
->get()
|
||||||
|
->map(function ($item) {
|
||||||
|
return [
|
||||||
|
'id' => $item->id,
|
||||||
|
'tanggal' => $item->created_at->format('d M Y'),
|
||||||
|
'nama_siswa' => $item->user ? $item->user->name : 'Siswa Tidak Diketahui',
|
||||||
|
'hasil' => $item->hasil_rekomendasi_jurusan ?? 'Belum ada hasil'
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => $riwayat
|
||||||
|
], 200);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Error: ' . $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mengambil data detail riwayat prediksi berdasarkan ID
|
||||||
|
*/
|
||||||
|
public function show($id)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$detail = RiwayatRekomendasi::with('user:id,name,email')->findOrFail($id);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => [
|
||||||
|
'id' => $detail->id,
|
||||||
|
'tanggal' => $detail->created_at->format('d M Y H:i:s'),
|
||||||
|
'nama_siswa' => $detail->user ? $detail->user->name : 'Siswa Tidak Diketahui',
|
||||||
|
'email_siswa' => $detail->user ? $detail->user->email : '-',
|
||||||
|
'hasil_rekomendasi' => $detail->hasil_rekomendasi_jurusan ?? 'Belum ada hasil',
|
||||||
|
'detail_lengkap' => $detail
|
||||||
|
]
|
||||||
|
], 200);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Data detail tidak ditemukan: ' . $e->getMessage()
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Siswa;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
|
||||||
|
class SiswaController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$siswa = Siswa::with(['user', 'jurusan'])->get();
|
||||||
|
return response()->json(['success' => true, 'data' => $siswa], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$validator = Validator::make($request->all(), [
|
||||||
|
'user_id' => 'required|exists:users,id|unique:siswas,user_id',
|
||||||
|
'jurusan_id' => 'required|exists:jurusans,id',
|
||||||
|
'nama_lengkap' => 'required|string|max:255',
|
||||||
|
'nisn' => 'required|string|unique:siswas,nisn',
|
||||||
|
'kelas' => 'required|string',
|
||||||
|
'peminatan' => 'required|string',
|
||||||
|
], [
|
||||||
|
// Pesan Error Spesifik
|
||||||
|
'user_id.unique' => 'Gagal! Akun User ID ini sudah terikat dengan data siswa lain.',
|
||||||
|
'nisn.unique' => 'Gagal! NISN ' . $request->nisn . ' sudah terdaftar di sistem.',
|
||||||
|
'nama_lengkap.required' => 'Kolom nama lengkap tidak boleh dikosongkan.',
|
||||||
|
'nisn.required' => 'Kolom NISN wajib diisi.',
|
||||||
|
'user_id.exists' => 'User ID yang Anda masukkan tidak terdaftar di database.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Validasi gagal',
|
||||||
|
'errors' => $validator->errors()
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$siswa = Siswa::create($request->all());
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Siswa ' . $siswa->id_siswa . ' (' . $siswa->nama_lengkap . ') berhasil ditambahkan.',
|
||||||
|
'data' => $siswa
|
||||||
|
], 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show($id)
|
||||||
|
{
|
||||||
|
$siswa = Siswa::find($id);
|
||||||
|
if (!$siswa) return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
|
||||||
|
return response()->json(['success' => true, 'data' => $siswa], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, $id)
|
||||||
|
{
|
||||||
|
$siswa = Siswa::find($id);
|
||||||
|
if (!$siswa) return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
|
||||||
|
|
||||||
|
$validator = Validator::make($request->all(), [
|
||||||
|
'nama_lengkap' => 'required|string|max:255',
|
||||||
|
'nisn' => 'required|string|unique:siswas,nisn,' . $id,
|
||||||
|
'kelas' => 'required|string',
|
||||||
|
], [
|
||||||
|
'nisn.unique' => 'Gagal Update! NISN ' . $request->nisn . ' sudah digunakan siswa lain.'
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$siswa->update($request->all());
|
||||||
|
return response()->json(['success' => true, 'message' => 'Data ' . $siswa->id_siswa . ' berhasil diperbarui'], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy($id)
|
||||||
|
{
|
||||||
|
$siswa = Siswa::find($id);
|
||||||
|
if (!$siswa) return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
|
||||||
|
$siswa->delete();
|
||||||
|
return response()->json(['success' => true, 'message' => 'Data berhasil dihapus'], 200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
|
class AuthController extends Controller
|
||||||
|
{
|
||||||
|
public function login(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'username' => 'required',
|
||||||
|
'password' => 'required',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user = User::where('username', $request->username)->first();
|
||||||
|
|
||||||
|
if (! $user || ! Hash::check($request->password, $user->password)) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Username atau password salah.'
|
||||||
|
], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hapus token lama
|
||||||
|
$user->tokens()->delete();
|
||||||
|
|
||||||
|
// Buat token baru
|
||||||
|
$token = $user->createToken('auth_token')->plainTextToken;
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Login berhasil',
|
||||||
|
'access_token' => $token,
|
||||||
|
'token_type' => 'Bearer',
|
||||||
|
'role' => $user->role, // Penting untuk redirect di frontend
|
||||||
|
'user' => [
|
||||||
|
'name' => $user->name,
|
||||||
|
'username' => $user->username,
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function logout(Request $request)
|
||||||
|
{
|
||||||
|
$request->user()->currentAccessToken()->delete();
|
||||||
|
return response()->json(['message' => 'Berhasil logout']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\Student;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\RiwayatRekomendasi;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class DashboardStudentController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$user = Auth::user();
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
return response()->json(['success' => false, 'message' => 'Unauthorized'], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
$totalAktivitas = RiwayatRekomendasi::where('user_id', $user->id)->count();
|
||||||
|
|
||||||
|
$rekomendasiTerakhir = RiwayatRekomendasi::where('user_id', $user->id)
|
||||||
|
->latest()
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$aktivitasTerbaru = RiwayatRekomendasi::where('user_id', $user->id)
|
||||||
|
->latest()
|
||||||
|
->take(5)
|
||||||
|
->get()
|
||||||
|
->map(function ($item) {
|
||||||
|
return [
|
||||||
|
'id' => $item->id,
|
||||||
|
'jurusan' => $item->hasil_rekomendasi_jurusan,
|
||||||
|
'tanggal' => $item->created_at->diffForHumans(),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
$trenNilai = RiwayatRekomendasi::where('user_id', $user->id)
|
||||||
|
->orderBy('created_at', 'asc')
|
||||||
|
->get()
|
||||||
|
->map(function ($item, $index) {
|
||||||
|
$mapel = [
|
||||||
|
'agama', 'pkn', 'bahasa_indonesia', 'bahasa_inggris', 'matematika',
|
||||||
|
'fisika', 'kimia', 'biologi', 'sejarah', 'geografi',
|
||||||
|
'ekonomi_mapel', 'sosiologi', 'seni_budaya', 'pjok', 'informatika'
|
||||||
|
];
|
||||||
|
|
||||||
|
$totalNilai = 0;
|
||||||
|
foreach ($mapel as $m) {
|
||||||
|
$totalNilai += (float) $item->$m;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rataRata = $totalNilai / count($mapel);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'label' => 'Tes ' . ($index + 1),
|
||||||
|
'nilai' => round($rataRata, 2)
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => [
|
||||||
|
'total_aktivitas' => $totalAktivitas,
|
||||||
|
'rekomendasi_terakhir' => $rekomendasiTerakhir ? $rekomendasiTerakhir->hasil_rekomendasi_jurusan : 'Belum Ada Data',
|
||||||
|
'aktivitas_terbaru' => $aktivitasTerbaru,
|
||||||
|
'tren_nilai' => $trenNilai
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Error: ' . $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,136 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\Student;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\RiwayatRekomendasi;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
|
class RecommendationApiController extends Controller
|
||||||
|
{
|
||||||
|
public function hitungPrediksi(Request $request)
|
||||||
|
{
|
||||||
|
$user = Auth::user();
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'User belum login.'
|
||||||
|
], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'agama' => 'required|numeric|min:0|max:100',
|
||||||
|
'pkn' => 'required|numeric|min:0|max:100',
|
||||||
|
'bahasa_indonesia' => 'required|numeric|min:0|max:100',
|
||||||
|
'bahasa_inggris' => 'required|numeric|min:0|max:100',
|
||||||
|
'matematika' => 'required|numeric|min:0|max:100',
|
||||||
|
'fisika' => 'required|numeric|min:0|max:100',
|
||||||
|
'kimia' => 'required|numeric|min:0|max:100',
|
||||||
|
'biologi' => 'required|numeric|min:0|max:100',
|
||||||
|
'sejarah' => 'required|numeric|min:0|max:100',
|
||||||
|
'geografi' => 'required|numeric|min:0|max:100',
|
||||||
|
'ekonomi_mapel' => 'required|numeric|min:0|max:100',
|
||||||
|
'sosiologi' => 'required|numeric|min:0|max:100',
|
||||||
|
'seni_budaya' => 'required|numeric|min:0|max:100',
|
||||||
|
'pjok' => 'required|numeric|min:0|max:100',
|
||||||
|
'informatika' => 'required|numeric|min:0|max:100',
|
||||||
|
|
||||||
|
'riasec_r' => 'required|numeric|min:0|max:100',
|
||||||
|
'riasec_i' => 'required|numeric|min:0|max:100',
|
||||||
|
'riasec_a' => 'required|numeric|min:0|max:100',
|
||||||
|
'riasec_s' => 'required|numeric|min:0|max:100',
|
||||||
|
'riasec_e' => 'required|numeric|min:0|max:100',
|
||||||
|
'riasec_c' => 'required|numeric|min:0|max:100',
|
||||||
|
|
||||||
|
'gaji_ortu' => 'required|numeric|min:0',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$gaji = (float) $validated['gaji_ortu'];
|
||||||
|
|
||||||
|
if ($gaji < 2000000) {
|
||||||
|
$ekonomiOrangTua = 'Kurang Mampu';
|
||||||
|
} elseif ($gaji <= 5000000) {
|
||||||
|
$ekonomiOrangTua = 'Menengah';
|
||||||
|
} else {
|
||||||
|
$ekonomiOrangTua = 'Mampu';
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = $validated;
|
||||||
|
$payload['ekonomi_orang_tua'] = $ekonomiOrangTua;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$response = Http::timeout(15)
|
||||||
|
->acceptJson()
|
||||||
|
->post('http://127.0.0.1:5000/predict', $payload);
|
||||||
|
|
||||||
|
if (!$response->successful()) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'ML Engine Error: ' . ($response->json()['message'] ?? $response->body())
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
$responseData = $response->json();
|
||||||
|
|
||||||
|
if (!isset($responseData['success']) || !$responseData['success']) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'ML Engine Error: ' . ($responseData['message'] ?? 'Unknown Error')
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
$teksJurusan = $responseData['prediksi_jurusan'] ?? null;
|
||||||
|
|
||||||
|
if (!$teksJurusan) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Prediksi jurusan tidak ditemukan pada respons Flask.'
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
$riwayat = RiwayatRekomendasi::create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'agama' => $validated['agama'],
|
||||||
|
'pkn' => $validated['pkn'],
|
||||||
|
'bahasa_indonesia' => $validated['bahasa_indonesia'],
|
||||||
|
'bahasa_inggris' => $validated['bahasa_inggris'],
|
||||||
|
'matematika' => $validated['matematika'],
|
||||||
|
'fisika' => $validated['fisika'],
|
||||||
|
'kimia' => $validated['kimia'],
|
||||||
|
'biologi' => $validated['biologi'],
|
||||||
|
'sejarah' => $validated['sejarah'],
|
||||||
|
'geografi' => $validated['geografi'],
|
||||||
|
'ekonomi_mapel' => $validated['ekonomi_mapel'],
|
||||||
|
'sosiologi' => $validated['sosiologi'],
|
||||||
|
'seni_budaya' => $validated['seni_budaya'],
|
||||||
|
'pjok' => $validated['pjok'],
|
||||||
|
'informatika' => $validated['informatika'],
|
||||||
|
'riasec_r' => $validated['riasec_r'],
|
||||||
|
'riasec_i' => $validated['riasec_i'],
|
||||||
|
'riasec_a' => $validated['riasec_a'],
|
||||||
|
'riasec_s' => $validated['riasec_s'],
|
||||||
|
'riasec_e' => $validated['riasec_e'],
|
||||||
|
'riasec_c' => $validated['riasec_c'],
|
||||||
|
'gaji_ortu' => $validated['gaji_ortu'],
|
||||||
|
'hasil_rekomendasi_jurusan' => $teksJurusan,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Prediksi berhasil dibuat.',
|
||||||
|
'redirect_url' => route('student.result', ['id' => $riwayat->id]),
|
||||||
|
'prediksi_jurusan' => $teksJurusan,
|
||||||
|
'top_predictions' => $responseData['top_predictions'] ?? null
|
||||||
|
]);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Koneksi ke API Flask Python gagal: ' . $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
// use App\Models\Alumni; // Un-comment jika Anda sudah memiliki model Alumni
|
||||||
|
// use App\Models\Jurusan; // Un-comment jika Anda sudah memiliki model Jurusan
|
||||||
|
|
||||||
|
class LandingPageController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Menampilkan halaman utama (Landing Page).
|
||||||
|
*
|
||||||
|
* @return \Illuminate\View\View
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'total_alumni' => 120,
|
||||||
|
'total_jurusan' => 15,
|
||||||
|
'rumpun_ilmu' => [
|
||||||
|
'Sains & Teknologi',
|
||||||
|
'Sosial & Humaniora',
|
||||||
|
'Ekonomi & Bisnis',
|
||||||
|
'Seni & Desain'
|
||||||
|
],
|
||||||
|
'masalah_nasional' => '87%'
|
||||||
|
];
|
||||||
|
|
||||||
|
return view('landingpage', compact('data'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class PanduanController extends Controller
|
||||||
|
{
|
||||||
|
// Hanya menampilkan halaman panduan
|
||||||
|
public function showPanduan()
|
||||||
|
{
|
||||||
|
return view('panduan');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Student;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use App\Models\RiwayatRekomendasi;
|
||||||
|
|
||||||
|
class RecommendationController extends Controller
|
||||||
|
{
|
||||||
|
// Menampilkan halaman form terpadu (Kuesioner + Nilai)
|
||||||
|
public function prediction()
|
||||||
|
{
|
||||||
|
return view('student.prediksi');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Menampilkan halaman hasil khusus berdasarkan ID
|
||||||
|
public function result($id)
|
||||||
|
{
|
||||||
|
$riwayat = RiwayatRekomendasi::findOrFail($id);
|
||||||
|
|
||||||
|
// Informasi Prospek Kerja berdasarkan hasil (Bisa kamu sesuaikan teksnya)
|
||||||
|
$prospekKerja = "Prospek karier untuk jurusan ini sangat luas, mulai dari spesialis industri, konsultan ahli, hingga pegawai instansi pemerintah/BUMN di sektor terkait.";
|
||||||
|
|
||||||
|
return view('student.hasil', compact('riwayat', 'prospekKerja'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Student;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class StudentDashboardController extends Controller
|
||||||
|
{
|
||||||
|
public function profile()
|
||||||
|
{
|
||||||
|
// Variabel untuk dashboard (Gambar 1-4)
|
||||||
|
$totalPrediksi = 0; // Contoh count dari tabel riwayat_rekomendasi
|
||||||
|
$isDataComplete = false; // Logika cek tabel nilai_rapot
|
||||||
|
|
||||||
|
return view('student.dashboard', compact('totalPrediksi', 'isDataComplete'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class AuthWebController extends Controller
|
||||||
|
{
|
||||||
|
// Hanya menampilkan halaman login
|
||||||
|
public function showLogin()
|
||||||
|
{
|
||||||
|
return view('auth.login');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class RoleMiddleware
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle an incoming request.
|
||||||
|
*
|
||||||
|
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||||
|
*/
|
||||||
|
public function handle(Request $request, Closure $next, $role): Response
|
||||||
|
{
|
||||||
|
// Pastikan user sudah login dan memiliki role yang sesuai
|
||||||
|
if ($request->user() && $request->user()->role === $role) {
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jika tidak sesuai, kembalikan error 403 Forbidden
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Anda tidak memiliki akses ke halaman ini.'
|
||||||
|
], 403);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Alumni extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'id_alumni',
|
||||||
|
'nama_alumni',
|
||||||
|
'perguruan_tinggi',
|
||||||
|
'jurusan_diambil',
|
||||||
|
'profil_nilai',
|
||||||
|
'profil_minat'
|
||||||
|
];
|
||||||
|
|
||||||
|
protected static function boot()
|
||||||
|
{
|
||||||
|
parent::boot();
|
||||||
|
|
||||||
|
static::creating(function ($model) {
|
||||||
|
$latest = static::orderBy('id', 'desc')->first();
|
||||||
|
if (!$latest) {
|
||||||
|
$model->id_alumni = 'AL01';
|
||||||
|
} else {
|
||||||
|
$number = intval(substr($latest->id_alumni, 2)) + 1;
|
||||||
|
$model->id_alumni = 'AL' . str_pad($number, 2, '0', STR_PAD_LEFT);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Guru extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = ['id_guru', 'nama_guru'];
|
||||||
|
|
||||||
|
protected static function boot()
|
||||||
|
{
|
||||||
|
parent::boot();
|
||||||
|
|
||||||
|
static::creating(function ($model) {
|
||||||
|
$latest = static::orderBy('id', 'desc')->first();
|
||||||
|
if (!$latest) {
|
||||||
|
$model->id_guru = 'GR01';
|
||||||
|
} else {
|
||||||
|
$number = intval(substr($latest->id_guru, 2)) + 1;
|
||||||
|
$model->id_guru = 'GR' . str_pad($number, 2, '0', STR_PAD_LEFT);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Jurusan extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = ['nama_jurusan', 'rumpun', 'deskripsi'];
|
||||||
|
|
||||||
|
// Relasi: Satu Jurusan memiliki banyak Prospek Kerja
|
||||||
|
public function prospeks()
|
||||||
|
{
|
||||||
|
return $this->hasMany(ProspekKerja::class, 'jurusan_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class ProspekKerja extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = ['jurusan_id', 'nama_profesi', 'range_gaji', 'deskripsi_pekerjaan'];
|
||||||
|
|
||||||
|
// Relasi: Satu profesi dimiliki oleh satu Jurusan (BelongsTo)
|
||||||
|
public function jurusan()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Jurusan::class, 'jurusan_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class RiwayatRekomendasi extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $guarded = ['id'];
|
||||||
|
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Siswa extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'id_siswa',
|
||||||
|
'user_id',
|
||||||
|
'jurusan_id',
|
||||||
|
'nama_lengkap',
|
||||||
|
'nisn',
|
||||||
|
'kelas',
|
||||||
|
'peminatan',
|
||||||
|
'minat_bakat',
|
||||||
|
'ekonomi'
|
||||||
|
];
|
||||||
|
|
||||||
|
protected static function boot()
|
||||||
|
{
|
||||||
|
parent::boot();
|
||||||
|
|
||||||
|
// Logika otomatisasi ID Siswa (SW01, SW02, dst)
|
||||||
|
static::creating(function ($model) {
|
||||||
|
$latestSiswa = static::orderBy('id', 'desc')->first();
|
||||||
|
|
||||||
|
if (!$latestSiswa) {
|
||||||
|
$model->id_siswa = 'SW01';
|
||||||
|
} else {
|
||||||
|
// Mengambil angka di belakang 'SW' dan menambahkannya 1
|
||||||
|
$number = intval(substr($latestSiswa->id_siswa, 2)) + 1;
|
||||||
|
$model->id_siswa = 'SW' . str_pad($number, 2, '0', STR_PAD_LEFT);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function user() {
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function jurusan() {
|
||||||
|
return $this->belongsTo(Jurusan::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,11 +6,13 @@
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
|
use Laravel\Sanctum\HasApiTokens;
|
||||||
|
|
||||||
class User extends Authenticatable
|
class User extends Authenticatable
|
||||||
{
|
{
|
||||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||||
use HasFactory, Notifiable;
|
use HasFactory, Notifiable;
|
||||||
|
use HasApiTokens, HasFactory, Notifiable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The attributes that are mass assignable.
|
* The attributes that are mass assignable.
|
||||||
|
|
@ -19,8 +21,10 @@ class User extends Authenticatable
|
||||||
*/
|
*/
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'name',
|
'name',
|
||||||
|
'username',
|
||||||
'email',
|
'email',
|
||||||
'password',
|
'password',
|
||||||
|
'role',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
# %% [1] IMPORT LIBRARIES & SETUP
|
||||||
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
print("--- [STEP 1] Memuat Data 15 Mapel & Kuesioner RIASEC ---")
|
||||||
|
# Set seed agar hasil random selalu sama setiap kali dijalankan
|
||||||
|
np.random.seed(42)
|
||||||
|
|
||||||
|
# %% [2] FUNGSI GENERATOR DATA REALISTIS (30 JURUSAN & RIASEC)
|
||||||
|
def generate_realistic_data(n_samples_per_class=200):
|
||||||
|
rows = []
|
||||||
|
|
||||||
|
# Konfigurasi 30 Jurusan dengan fokus Mapel dan profil RIASEC dominan
|
||||||
|
jurusan_config = {
|
||||||
|
'Kedokteran': {'mapel': ['biologi', 'kimia', 'matematika'], 'riasec': ['I', 'S'], 'eko_min': ['Mampu', 'Menengah']},
|
||||||
|
'Farmasi': {'mapel': ['kimia', 'biologi', 'matematika'], 'riasec': ['I', 'C'], 'eko_min': ['Mampu', 'Menengah', 'Kurang Mampu']},
|
||||||
|
'Kesehatan Masyarakat': {'mapel': ['sosiologi', 'biologi'], 'riasec': ['S', 'E'], 'eko_min': ['Mampu', 'Menengah', 'Kurang Mampu']},
|
||||||
|
'Ilmu Gizi': {'mapel': ['biologi', 'kimia'], 'riasec': ['I', 'S'], 'eko_min': ['Mampu', 'Menengah', 'Kurang Mampu']},
|
||||||
|
'Keperawatan': {'mapel': ['biologi', 'sosiologi'], 'riasec': ['S', 'R'], 'eko_min': ['Mampu', 'Menengah', 'Kurang Mampu']},
|
||||||
|
|
||||||
|
'Teknik Sipil': {'mapel': ['matematika', 'fisika'], 'riasec': ['R', 'C'], 'eko_min': ['Mampu', 'Menengah', 'Kurang Mampu']},
|
||||||
|
'Teknik Mesin': {'mapel': ['fisika', 'matematika'], 'riasec': ['R', 'I'], 'eko_min': ['Mampu', 'Menengah', 'Kurang Mampu']},
|
||||||
|
'Teknik Industri': {'mapel': ['fisika', 'ekonomi_mapel'], 'riasec': ['E', 'I'], 'eko_min': ['Mampu', 'Menengah', 'Kurang Mampu']},
|
||||||
|
'Teknik Pertambangan': {'mapel': ['fisika', 'geografi', 'matematika'], 'riasec': ['R', 'E'], 'eko_min': ['Mampu', 'Menengah']},
|
||||||
|
'Teknik Geologi': {'mapel': ['geografi', 'fisika', 'kimia'], 'riasec': ['I', 'R'], 'eko_min': ['Mampu', 'Menengah']},
|
||||||
|
|
||||||
|
'Teknik Informatika': {'mapel': ['informatika', 'matematika'], 'riasec': ['I', 'C'], 'eko_min': ['Mampu', 'Menengah', 'Kurang Mampu']},
|
||||||
|
'Sistem Informasi': {'mapel': ['informatika', 'ekonomi_mapel'], 'riasec': ['C', 'I'], 'eko_min': ['Mampu', 'Menengah', 'Kurang Mampu']},
|
||||||
|
|
||||||
|
'Pertanian': {'mapel': ['biologi', 'geografi'], 'riasec': ['R', 'I'], 'eko_min': ['Mampu', 'Menengah', 'Kurang Mampu']},
|
||||||
|
'Peternakan': {'mapel': ['biologi', 'ekonomi_mapel'], 'riasec': ['R', 'E'], 'eko_min': ['Mampu', 'Menengah', 'Kurang Mampu']},
|
||||||
|
'Agribisnis': {'mapel': ['ekonomi_mapel', 'biologi'], 'riasec': ['E', 'R'], 'eko_min': ['Mampu', 'Menengah', 'Kurang Mampu']},
|
||||||
|
|
||||||
|
'Akuntansi': {'mapel': ['ekonomi_mapel', 'matematika'], 'riasec': ['C', 'E'], 'eko_min': ['Mampu', 'Menengah', 'Kurang Mampu']},
|
||||||
|
'Manajemen': {'mapel': ['ekonomi_mapel', 'sosiologi'], 'riasec': ['E', 'C'], 'eko_min': ['Mampu', 'Menengah', 'Kurang Mampu']},
|
||||||
|
'Ilmu Ekonomi': {'mapel': ['ekonomi_mapel', 'matematika', 'sejarah'], 'riasec': ['I', 'C'], 'eko_min': ['Mampu', 'Menengah', 'Kurang Mampu']},
|
||||||
|
|
||||||
|
'Ilmu Hukum': {'mapel': ['pkn', 'sosiologi', 'bahasa_indonesia'], 'riasec': ['E', 'S'], 'eko_min': ['Mampu', 'Menengah', 'Kurang Mampu']},
|
||||||
|
'Hubungan Internasional': {'mapel': ['bahasa_inggris', 'sejarah', 'pkn'], 'riasec': ['E', 'S'], 'eko_min': ['Mampu', 'Menengah']},
|
||||||
|
'Ilmu Komunikasi': {'mapel': ['bahasa_indonesia', 'sosiologi', 'bahasa_inggris'], 'riasec': ['E', 'A'], 'eko_min': ['Mampu', 'Menengah', 'Kurang Mampu']},
|
||||||
|
'Psikologi': {'mapel': ['sosiologi', 'bahasa_indonesia', 'biologi'], 'riasec': ['S', 'I'], 'eko_min': ['Mampu', 'Menengah', 'Kurang Mampu']},
|
||||||
|
'Administrasi Negara': {'mapel': ['pkn', 'sosiologi'], 'riasec': ['C', 'E'], 'eko_min': ['Mampu', 'Menengah', 'Kurang Mampu']},
|
||||||
|
|
||||||
|
'Desain Komunikasi Visual': {'mapel': ['seni_budaya', 'informatika'], 'riasec': ['A', 'E'], 'eko_min': ['Mampu', 'Menengah']},
|
||||||
|
'Arsitektur': {'mapel': ['seni_budaya', 'fisika', 'matematika'], 'riasec': ['A', 'I'], 'eko_min': ['Mampu', 'Menengah']},
|
||||||
|
'Sastra Inggris': {'mapel': ['bahasa_inggris', 'sejarah', 'seni_budaya'], 'riasec': ['A', 'S'], 'eko_min': ['Mampu', 'Menengah', 'Kurang Mampu']},
|
||||||
|
|
||||||
|
'Pendidikan Guru SD': {'mapel': ['pkn', 'bahasa_indonesia', 'seni_budaya'], 'riasec': ['S', 'C'], 'eko_min': ['Mampu', 'Menengah', 'Kurang Mampu']},
|
||||||
|
'Pendidikan Matematika': {'mapel': ['matematika', 'pkn'], 'riasec': ['S', 'I'], 'eko_min': ['Mampu', 'Menengah', 'Kurang Mampu']},
|
||||||
|
'Pendidikan Jasmani': {'mapel': ['pjok', 'biologi'], 'riasec': ['S', 'R'], 'eko_min': ['Mampu', 'Menengah', 'Kurang Mampu']},
|
||||||
|
|
||||||
|
'Pariwisata': {'mapel': ['bahasa_inggris', 'geografi', 'seni_budaya'], 'riasec': ['E', 'S'], 'eko_min': ['Mampu', 'Menengah', 'Kurang Mampu']}
|
||||||
|
}
|
||||||
|
|
||||||
|
siswa_id = 1
|
||||||
|
|
||||||
|
for jurusan, config in jurusan_config.items():
|
||||||
|
for _ in range(n_samples_per_class):
|
||||||
|
# 1. Base Scores Mapel (Acak Normal 75-84)
|
||||||
|
scores = {k: np.random.randint(75, 85) for k in [
|
||||||
|
'agama', 'pkn', 'bahasa_indonesia', 'bahasa_inggris', 'matematika',
|
||||||
|
'fisika', 'kimia', 'biologi', 'sejarah', 'geografi',
|
||||||
|
'ekonomi_mapel', 'sosiologi', 'seni_budaya', 'pjok', 'informatika'
|
||||||
|
]}
|
||||||
|
# Naikkan skor mapel utama jurusan tersebut
|
||||||
|
for m in config['mapel']:
|
||||||
|
scores[m] = np.random.randint(88, 98)
|
||||||
|
|
||||||
|
# 2. Base Scores Kuesioner RIASEC (Acak Normal 50-70)
|
||||||
|
riasec = {k: np.random.randint(50, 71) for k in ['R', 'I', 'A', 'S', 'E', 'C']}
|
||||||
|
# Naikkan skor RIASEC dominan jurusan tersebut
|
||||||
|
for r_key in config['riasec']:
|
||||||
|
riasec[r_key] = np.random.randint(85, 98)
|
||||||
|
|
||||||
|
# 3. Ekonomi Orang Tua
|
||||||
|
eko = np.random.choice(config['eko_min'])
|
||||||
|
|
||||||
|
# 4. Susun Baris Data
|
||||||
|
row = [f"SW{siswa_id:05d}", f"Siswa_{siswa_id}"] + \
|
||||||
|
list(scores.values()) + \
|
||||||
|
[riasec['R'], riasec['I'], riasec['A'], riasec['S'], riasec['E'], riasec['C']] + \
|
||||||
|
[eko, jurusan]
|
||||||
|
|
||||||
|
rows.append(row)
|
||||||
|
siswa_id += 1
|
||||||
|
|
||||||
|
columns = ['id_siswa', 'nama_lengkap',
|
||||||
|
'agama', 'pkn', 'bahasa_indonesia', 'bahasa_inggris', 'matematika',
|
||||||
|
'fisika', 'kimia', 'biologi', 'sejarah', 'geografi',
|
||||||
|
'ekonomi_mapel', 'sosiologi', 'seni_budaya', 'pjok', 'informatika',
|
||||||
|
'riasec_r', 'riasec_i', 'riasec_a', 'riasec_s', 'riasec_e', 'riasec_c',
|
||||||
|
'ekonomi_orang_tua', 'rekomendasi_jurusan']
|
||||||
|
|
||||||
|
# Acak urutan baris data agar tidak terurut per jurusan saat di training
|
||||||
|
df = pd.DataFrame(rows, columns=columns)
|
||||||
|
df = df.sample(frac=1, random_state=42).reset_index(drop=True)
|
||||||
|
return df
|
||||||
|
|
||||||
|
# %% [3] EKSEKUSI DAN SIMPAN DATA
|
||||||
|
# Mengambil 200 sampel per jurusan x 30 jurusan = 6.000 data latih
|
||||||
|
df_synthetic = generate_realistic_data(n_samples_per_class=200)
|
||||||
|
df_synthetic.to_csv('dataset_15mapel_bersih.csv', index=False)
|
||||||
|
print(f"Dataset berhasil dibuat dengan total {len(df_synthetic)} baris!")
|
||||||
|
print("Fitur baru: Skor RIASEC ditambahkan untuk 30 Jurusan.")
|
||||||
|
# %%
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,80 @@
|
||||||
|
# %% [1] IMPORT LIBRARIES & LOAD DATA
|
||||||
|
import pandas as pd
|
||||||
|
import seaborn as sns
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
from sklearn.preprocessing import LabelEncoder
|
||||||
|
import joblib
|
||||||
|
|
||||||
|
# Mengatur tema visualisasi agar terlihat profesional untuk laporan TA
|
||||||
|
plt.style.use('seaborn-v0_8-whitegrid')
|
||||||
|
|
||||||
|
print("--- [STEP 2] EDA, Visualisasi, & Preprocessing (Integrasi RIASEC) ---")
|
||||||
|
df = pd.read_csv('dataset_15mapel_bersih.csv')
|
||||||
|
|
||||||
|
# %% [2] VISUALISASI 1: DISTRIBUSI KELAS (SEBELUM ENCODING)
|
||||||
|
print("\nMenampilkan Grafik Distribusi Jurusan...")
|
||||||
|
plt.figure(figsize=(12, 10))
|
||||||
|
# Membuat grafik batang horizontal agar 30 nama jurusan terbaca jelas
|
||||||
|
sns.countplot(y='rekomendasi_jurusan', data=df,
|
||||||
|
order=df['rekomendasi_jurusan'].value_counts().index,
|
||||||
|
palette='mako')
|
||||||
|
plt.title('Distribusi Jumlah Siswa per Jurusan (Memastikan Data Seimbang)', fontsize=16, fontweight='bold')
|
||||||
|
plt.xlabel('Jumlah Siswa', fontsize=12)
|
||||||
|
plt.ylabel('Jurusan', fontsize=12)
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
# %% [3] PREPROCESSING TAHAP 1: HAPUS KOLOM TEKS (IDENTITAS)
|
||||||
|
print("\nMelakukan Preprocessing Dasar...")
|
||||||
|
|
||||||
|
# Kolom kategori teks yang akan kita pertahankan untuk di-encode
|
||||||
|
kolom_kategori_penting = ['ekonomi_orang_tua', 'rekomendasi_jurusan']
|
||||||
|
|
||||||
|
# Hapus kolom id_siswa dan nama_lengkap karena mesin tidak belajar dari nama/ID
|
||||||
|
for col in df.select_dtypes(include=['object']).columns:
|
||||||
|
if col not in kolom_kategori_penting:
|
||||||
|
df = df.drop(columns=[col])
|
||||||
|
print(f"✅ Kolom '{col}' berhasil dibuang.")
|
||||||
|
|
||||||
|
df = df.dropna()
|
||||||
|
|
||||||
|
# %% [4] PREPROCESSING TAHAP 2: LABEL ENCODING
|
||||||
|
print("\nMelakukan Encoding (Mengubah Teks menjadi Angka)...")
|
||||||
|
encoders = {}
|
||||||
|
|
||||||
|
for col in kolom_kategori_penting:
|
||||||
|
if col in df.columns:
|
||||||
|
le = LabelEncoder()
|
||||||
|
df[col] = le.fit_transform(df[col])
|
||||||
|
encoders[col] = le
|
||||||
|
print(f"✅ Kamus '{col}' tersimpan. Jumlah kelas: {len(le.classes_)}")
|
||||||
|
|
||||||
|
# Simpan encoder untuk digunakan di web/aplikasi (sangat penting untuk decode di PHP Laravel)
|
||||||
|
joblib.dump(encoders, 'encoders_15mapel.pkl')
|
||||||
|
|
||||||
|
# %% [5] VISUALISASI 2: HEATMAP KORELASI (SETELAH ENCODING)
|
||||||
|
print("\nMenampilkan Heatmap Korelasi Antar Fitur...")
|
||||||
|
plt.figure(figsize=(22, 18)) # Ukuran diperbesar karena fiturnya banyak (21 fitur)
|
||||||
|
|
||||||
|
# Membuat korelasi pearson
|
||||||
|
corr_matrix = df.corr()
|
||||||
|
|
||||||
|
# Menampilkan Heatmap
|
||||||
|
sns.heatmap(corr_matrix, annot=True, fmt='.2f', cmap='coolwarm',
|
||||||
|
vmin=-1, vmax=1, square=True, linewidths=.5, annot_kws={"size": 8})
|
||||||
|
plt.title('Heatmap Korelasi: 15 Mapel, 6 Skor RIASEC, Ekonomi, dan Target Jurusan', fontsize=18, fontweight='bold')
|
||||||
|
plt.xticks(rotation=45, ha='right', fontsize=10)
|
||||||
|
plt.yticks(fontsize=10)
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
# %% [6] MENYIMPAN DAFTAR FITUR & DATASET BERSIH
|
||||||
|
print("\nMenyimpan urutan kolom (features) agar saat testing di Laravel tidak tertukar posisinya...")
|
||||||
|
feature_columns = [col for col in df.columns if col != 'rekomendasi_jurusan']
|
||||||
|
joblib.dump(feature_columns, 'feature_columns_15mapel.pkl')
|
||||||
|
print(f"✅ Urutan fitur tersimpan: {feature_columns}")
|
||||||
|
|
||||||
|
# Simpan data yang sudah berupa angka semua (termasuk 6 kolom riasec)
|
||||||
|
df.to_csv('dataset_siswa_siap_model.csv', index=False)
|
||||||
|
print("\nData siap latih berhasil disimpan sebagai 'dataset_siswa_siap_model.csv'!")
|
||||||
|
# %%
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,68 @@
|
||||||
|
# %% [1] IMPORT LIBRARIES & LOAD DATA
|
||||||
|
import pandas as pd
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import seaborn as sns
|
||||||
|
from sklearn.model_selection import train_test_split
|
||||||
|
from sklearn.ensemble import RandomForestClassifier
|
||||||
|
from sklearn.metrics import classification_report, accuracy_score
|
||||||
|
import joblib
|
||||||
|
|
||||||
|
print("--- [STEP 3] Pelatihan Model Random Forest 30 Jurusan ---")
|
||||||
|
print("Memuat dataset yang sudah diproses...")
|
||||||
|
df = pd.read_csv('dataset_siswa_siap_model.csv')
|
||||||
|
|
||||||
|
# %% [2] MEMISAHKAN FITUR DAN TARGET (DATA SPLIT)
|
||||||
|
print("Membagi fitur dan target...")
|
||||||
|
X = df.drop('rekomendasi_jurusan', axis=1)
|
||||||
|
y = df['rekomendasi_jurusan']
|
||||||
|
|
||||||
|
print("Membagi data menjadi data latih (80%) dan data uji (20%)...")
|
||||||
|
# Karena ada 30 kelas, stratify sangat wajib agar distribusi per kelas merata
|
||||||
|
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)
|
||||||
|
print(f"Jumlah Data Latih: {X_train.shape[0]} | Jumlah Data Uji: {X_test.shape[0]}")
|
||||||
|
|
||||||
|
# %% [3] TRAINING MODEL RANDOM FOREST (HYPERPARAMETER ANTI-OVERFIT)
|
||||||
|
print("\nMelatih model Random Forest...")
|
||||||
|
# Dengan data dimensi RIASEC, kita bisa membebaskan pohon sedikit lebih dalam
|
||||||
|
rf_model = RandomForestClassifier(
|
||||||
|
n_estimators=200, # 200 pohon sudah sangat cukup untuk 30 jurusan
|
||||||
|
max_depth=12, # Cegah menghafal terlalu dalam (Anti Overfitting)
|
||||||
|
min_samples_split=4, # Split akan terjadi jika minimal ada 4 sampel
|
||||||
|
min_samples_leaf=2, # Daun terakhir minimal berisi 2 sampel
|
||||||
|
max_features='sqrt',
|
||||||
|
class_weight='balanced',
|
||||||
|
random_state=42,
|
||||||
|
n_jobs=-1
|
||||||
|
)
|
||||||
|
|
||||||
|
rf_model.fit(X_train, y_train)
|
||||||
|
print("Proses pelatihan selesai!")
|
||||||
|
|
||||||
|
# %% [4] EVALUASI MODEL
|
||||||
|
print("\nMengevaluasi performa model pada data uji...")
|
||||||
|
y_pred = rf_model.predict(X_test)
|
||||||
|
|
||||||
|
akurasi = accuracy_score(y_test, y_pred)
|
||||||
|
print(f"\n======================================")
|
||||||
|
print(f"AKURASI MODEL 30 JURUSAN: {akurasi * 100:.2f}%")
|
||||||
|
print(f"======================================")
|
||||||
|
|
||||||
|
print("\nLaporan Detail Prediksi Keseluruhan:")
|
||||||
|
print(classification_report(y_test, y_pred))
|
||||||
|
|
||||||
|
# Cek Feature Importances (Membuktikan bahwa RIASEC akan memiliki bobot besar)
|
||||||
|
importances = rf_model.feature_importances_
|
||||||
|
feature_names = X.columns
|
||||||
|
feature_imp_df = pd.DataFrame({'Fitur': feature_names, 'Bobot': importances}).sort_values('Bobot', ascending=False)
|
||||||
|
|
||||||
|
plt.figure(figsize=(10, 8))
|
||||||
|
sns.barplot(x='Bobot', y='Fitur', data=feature_imp_df, palette='magma')
|
||||||
|
plt.title('Tingkat Kepentingan Fitur (Feature Importances)', fontsize=14)
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
# %% [5] SIMPAN MODEL
|
||||||
|
print("\nMenyimpan struktur model yang sudah berhasil dilatih...")
|
||||||
|
joblib.dump(rf_model, 'model_rekomendasi_15mapel.pkl')
|
||||||
|
print("Model berhasil diamankan ke dalam format 'model_rekomendasi_15mapel.pkl'!")
|
||||||
|
# %%
|
||||||
|
|
@ -7,12 +7,15 @@
|
||||||
return Application::configure(basePath: dirname(__DIR__))
|
return Application::configure(basePath: dirname(__DIR__))
|
||||||
->withRouting(
|
->withRouting(
|
||||||
web: __DIR__.'/../routes/web.php',
|
web: __DIR__.'/../routes/web.php',
|
||||||
|
api: __DIR__.'/../routes/api.php',
|
||||||
commands: __DIR__.'/../routes/console.php',
|
commands: __DIR__.'/../routes/console.php',
|
||||||
health: '/up',
|
health: '/up',
|
||||||
)
|
)
|
||||||
->withMiddleware(function (Middleware $middleware): void {
|
->withMiddleware(function (Middleware $middleware) {
|
||||||
//
|
$middleware->alias([
|
||||||
|
'role' => \App\Http\Middleware\RoleMiddleware::class,
|
||||||
|
]);
|
||||||
})
|
})
|
||||||
->withExceptions(function (Exceptions $exceptions): void {
|
->withExceptions(function (Exceptions $exceptions) {
|
||||||
//
|
//
|
||||||
})->create();
|
})->create();
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.2",
|
"php": "^8.2",
|
||||||
"laravel/framework": "^12.0",
|
"laravel/framework": "^12.0",
|
||||||
|
"laravel/sanctum": "^4.3",
|
||||||
"laravel/tinker": "^2.10.1"
|
"laravel/tinker": "^2.10.1"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "c514d8f7b9fc5970bdd94287905ef584",
|
"content-hash": "ad91ab9bde70e3576f2b64dd11542bc4",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "brick/math",
|
"name": "brick/math",
|
||||||
|
|
@ -1333,6 +1333,69 @@
|
||||||
},
|
},
|
||||||
"time": "2026-01-13T20:29:29+00:00"
|
"time": "2026-01-13T20:29:29+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "laravel/sanctum",
|
||||||
|
"version": "v4.3.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/laravel/sanctum.git",
|
||||||
|
"reference": "e3b85d6e36ad00e5db2d1dcc27c81ffdf15cbf76"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/laravel/sanctum/zipball/e3b85d6e36ad00e5db2d1dcc27c81ffdf15cbf76",
|
||||||
|
"reference": "e3b85d6e36ad00e5db2d1dcc27c81ffdf15cbf76",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-json": "*",
|
||||||
|
"illuminate/console": "^11.0|^12.0|^13.0",
|
||||||
|
"illuminate/contracts": "^11.0|^12.0|^13.0",
|
||||||
|
"illuminate/database": "^11.0|^12.0|^13.0",
|
||||||
|
"illuminate/support": "^11.0|^12.0|^13.0",
|
||||||
|
"php": "^8.2",
|
||||||
|
"symfony/console": "^7.0|^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"mockery/mockery": "^1.6",
|
||||||
|
"orchestra/testbench": "^9.15|^10.8|^11.0",
|
||||||
|
"phpstan/phpstan": "^1.10"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"providers": [
|
||||||
|
"Laravel\\Sanctum\\SanctumServiceProvider"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Laravel\\Sanctum\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Taylor Otwell",
|
||||||
|
"email": "taylor@laravel.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.",
|
||||||
|
"keywords": [
|
||||||
|
"auth",
|
||||||
|
"laravel",
|
||||||
|
"sanctum"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/laravel/sanctum/issues",
|
||||||
|
"source": "https://github.com/laravel/sanctum"
|
||||||
|
},
|
||||||
|
"time": "2026-02-07T17:19:31+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "laravel/serializable-closure",
|
"name": "laravel/serializable-closure",
|
||||||
"version": "v2.0.8",
|
"version": "v2.0.8",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Laravel\Sanctum\Sanctum;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Stateful Domains
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Requests from the following domains / hosts will receive stateful API
|
||||||
|
| authentication cookies. Typically, these should include your local
|
||||||
|
| and production domains which access your API via a frontend SPA.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
|
||||||
|
'%s%s',
|
||||||
|
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
|
||||||
|
Sanctum::currentApplicationUrlWithPort(),
|
||||||
|
// Sanctum::currentRequestHost(),
|
||||||
|
))),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Sanctum Guards
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This array contains the authentication guards that will be checked when
|
||||||
|
| Sanctum is trying to authenticate a request. If none of these guards
|
||||||
|
| are able to authenticate the request, Sanctum will use the bearer
|
||||||
|
| token that's present on an incoming request for authentication.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'guard' => ['web'],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Expiration Minutes
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value controls the number of minutes until an issued token will be
|
||||||
|
| considered expired. This will override any values set in the token's
|
||||||
|
| "expires_at" attribute, but first-party sessions are not affected.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'expiration' => null,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Token Prefix
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Sanctum can prefix new tokens in order to take advantage of numerous
|
||||||
|
| security scanning initiatives maintained by open source platforms
|
||||||
|
| that notify developers if they commit tokens into repositories.
|
||||||
|
|
|
||||||
|
| See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Sanctum Middleware
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When authenticating your first-party SPA with Sanctum you may need to
|
||||||
|
| customize some of the middleware Sanctum uses while processing the
|
||||||
|
| request. You may change the middleware listed below as required.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'middleware' => [
|
||||||
|
'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class,
|
||||||
|
'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class,
|
||||||
|
'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -14,9 +14,11 @@ public function up(): void
|
||||||
Schema::create('users', function (Blueprint $table) {
|
Schema::create('users', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->string('name');
|
$table->string('name');
|
||||||
|
$table->string('username')->unique();
|
||||||
$table->string('email')->unique();
|
$table->string('email')->unique();
|
||||||
$table->timestamp('email_verified_at')->nullable();
|
$table->timestamp('email_verified_at')->nullable();
|
||||||
$table->string('password');
|
$table->string('password');
|
||||||
|
$table->string('role');
|
||||||
$table->rememberToken();
|
$table->rememberToken();
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('jurusans', function (Blueprint $table) {
|
||||||
|
$table->id(); // Mewakili Id_Jurusan
|
||||||
|
$table->string('nama_jurusan');
|
||||||
|
$table->string('rumpun');
|
||||||
|
$table->text('deskripsi')->nullable();
|
||||||
|
$table->text('prospek_karir')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('jurusans');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('siswas', function (Blueprint $table) {
|
||||||
|
$table->id(); // Primary Key Utama (BigInt)
|
||||||
|
$table->string('id_siswa')->unique();
|
||||||
|
$table->foreignId('user_id')->constrained('users')->onDelete('cascade');
|
||||||
|
$table->foreignId('jurusan_id')->constrained('jurusans')->onDelete('cascade');
|
||||||
|
$table->string('nama_lengkap');
|
||||||
|
$table->string('nisn')->unique();
|
||||||
|
$table->string('kelas');
|
||||||
|
$table->string('peminatan');
|
||||||
|
$table->string('minat_bakat')->nullable();
|
||||||
|
$table->string('ekonomi')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('siswas');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('gurus', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('id_guru')->unique(); // Tambahkan ID Bisnis
|
||||||
|
$table->string('nama_guru');
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('gurus');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('alumnis', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('id_alumni')->unique(); // Tambahkan ID Bisnis
|
||||||
|
$table->string('nama_alumni');
|
||||||
|
$table->string('perguruan_tinggi');
|
||||||
|
$table->string('jurusan_diambil');
|
||||||
|
$table->text('profil_nilai')->nullable();
|
||||||
|
$table->text('profil_minat')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('alumnis');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('personal_access_tokens', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->morphs('tokenable');
|
||||||
|
$table->text('name');
|
||||||
|
$table->string('token', 64)->unique();
|
||||||
|
$table->text('abilities')->nullable();
|
||||||
|
$table->timestamp('last_used_at')->nullable();
|
||||||
|
$table->timestamp('expires_at')->nullable()->index();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('personal_access_tokens');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration {
|
||||||
|
public function up(): void {
|
||||||
|
Schema::create('riwayat_rekomendasis', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete(); // Hilangkan komentar jika sudah pakai login Auth
|
||||||
|
|
||||||
|
// Kolom Profil
|
||||||
|
$table->integer('gaji_ortu');
|
||||||
|
$table->integer('ekonomi_orang_tua'); // 0: Kurang, 1: Menengah, 2: Mampu
|
||||||
|
$table->integer('minat_bakat'); // 0-3 sesuai dataset
|
||||||
|
|
||||||
|
// 15 Nilai Mata Pelajaran
|
||||||
|
$table->float('agama');
|
||||||
|
$table->float('pkn');
|
||||||
|
$table->float('bahasa_indonesia');
|
||||||
|
$table->float('bahasa_inggris');
|
||||||
|
$table->float('matematika');
|
||||||
|
$table->float('fisika');
|
||||||
|
$table->float('kimia');
|
||||||
|
$table->float('biologi');
|
||||||
|
$table->float('sejarah');
|
||||||
|
$table->float('geografi');
|
||||||
|
$table->float('ekonomi_mapel');
|
||||||
|
$table->float('sosiologi');
|
||||||
|
$table->float('seni_budaya');
|
||||||
|
$table->float('pjok');
|
||||||
|
$table->float('informatika');
|
||||||
|
|
||||||
|
// Hasil Akhir
|
||||||
|
$table->string('hasil_rekomendasi_jurusan')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void {
|
||||||
|
Schema::dropIfExists('riwayat_rekomendasis');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('prospek_kerjas', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
// Menghubungkan prospek kerja ke ID jurusan tertentu
|
||||||
|
$table->foreignId('jurusan_id')->constrained('jurusans')->onDelete('cascade');
|
||||||
|
$table->string('nama_profesi');
|
||||||
|
$table->string('range_gaji')->nullable(); // Contoh: Rp 7.000.000 - Rp 15.000.000
|
||||||
|
$table->text('deskripsi_pekerjaan')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('prospek_kerjas');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Models\Alumni;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class AlumniSeeder extends Seeder
|
||||||
|
{
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
|
||||||
|
// Bersihkan data lama agar ID reset ke AL01
|
||||||
|
DB::statement('SET FOREIGN_KEY_CHECKS=0;');
|
||||||
|
Alumni::truncate();
|
||||||
|
DB::statement('SET FOREIGN_KEY_CHECKS=1;');
|
||||||
|
|
||||||
|
$alumnis = [
|
||||||
|
[
|
||||||
|
'nama_alumni' => 'Andi Wijaya',
|
||||||
|
'perguruan_tinggi' => 'Universitas Indonesia',
|
||||||
|
'jurusan_diambil' => 'Ilmu Komputer',
|
||||||
|
'profil_nilai' => 'Matematika: 90, Fisika: 85',
|
||||||
|
'profil_minat' => 'Teknologi dan Logika'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'nama_alumni' => 'Bunga Citra',
|
||||||
|
'perguruan_tinggi' => 'Universitas Gadjah Mada',
|
||||||
|
'jurusan_diambil' => 'Akuntansi',
|
||||||
|
'profil_nilai' => 'Ekonomi: 95, Matematika: 88',
|
||||||
|
'profil_minat' => 'Keuangan dan Analisis'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'nama_alumni' => 'Candra Kirana',
|
||||||
|
'perguruan_tinggi' => 'Institut Teknologi Bandung',
|
||||||
|
'jurusan_diambil' => 'Teknik Informatika',
|
||||||
|
'profil_nilai' => 'Informatika: 92, Inggris: 85',
|
||||||
|
'profil_minat' => 'Programming'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'nama_alumni' => 'Dedi Setiadi',
|
||||||
|
'perguruan_tinggi' => 'Universitas Padjadjaran',
|
||||||
|
'jurusan_diambil' => 'Ilmu Komunikasi',
|
||||||
|
'profil_nilai' => 'Sosiologi: 88, B.Indonesia: 90',
|
||||||
|
'profil_minat' => 'Public Speaking'
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($alumnis as $a) {
|
||||||
|
Alumni::create($a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,23 +3,31 @@
|
||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
class DatabaseSeeder extends Seeder
|
class DatabaseSeeder extends Seeder
|
||||||
{
|
{
|
||||||
use WithoutModelEvents;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Seed the application's database.
|
|
||||||
*/
|
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
// User::factory(10)->create();
|
// 1. Buat 4 User untuk Siswa dengan menyertakan kolom 'name'
|
||||||
|
for ($i = 1; $i <= 4; $i++) {
|
||||||
|
User::create([
|
||||||
|
'name' => 'Siswa Contoh ' . $i, // Tambahkan baris ini
|
||||||
|
'username' => 'siswa' . $i,
|
||||||
|
'email' => 'siswa' . $i . '@example.com',
|
||||||
|
'password' => bcrypt('password'),
|
||||||
|
'role' => 'siswa'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
User::factory()->create([
|
// 2. Panggil Seeder lainnya
|
||||||
'name' => 'Test User',
|
$this->call([
|
||||||
'email' => 'test@example.com',
|
JurusanSeeder::class,
|
||||||
|
GuruSeeder::class,
|
||||||
|
AlumniSeeder::class,
|
||||||
|
SiswaSeeder::class,
|
||||||
|
UserSeeder::class,
|
||||||
|
ProspekKerjaSeeder::class,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Models\Guru;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class GuruSeeder extends Seeder
|
||||||
|
{
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
// Bersihkan data lama agar ID mulai dari GR01 lagi
|
||||||
|
DB::statement('SET FOREIGN_KEY_CHECKS=0;');
|
||||||
|
Guru::truncate();
|
||||||
|
DB::statement('SET FOREIGN_KEY_CHECKS=1;');
|
||||||
|
|
||||||
|
$gurus = [
|
||||||
|
['nama_guru' => 'Budi Santoso, S.Pd'],
|
||||||
|
['nama_guru' => 'Siti Aminah, M.Pd'],
|
||||||
|
['nama_guru' => 'Hendra Wijaya, S.Kom'],
|
||||||
|
['nama_guru' => 'Rina Kartika, M.Si'],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($gurus as $g) {
|
||||||
|
// Menggunakan Guru::create agar fungsi boot() di model terpanggil otomatis
|
||||||
|
Guru::create($g);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Models\Jurusan;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
|
class JurusanSeeder extends Seeder
|
||||||
|
{
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$data = [
|
||||||
|
[
|
||||||
|
'nama_jurusan' => 'Teknik Informatika',
|
||||||
|
'rumpun' => 'Saintek',
|
||||||
|
'deskripsi' => 'Fokus pada pengembangan perangkat lunak dan sistem komputer.',
|
||||||
|
'prospek_karir' => 'Software Engineer, Data Scientist, Web Developer'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'nama_jurusan' => 'Sistem Informasi',
|
||||||
|
'rumpun' => 'Saintek',
|
||||||
|
'deskripsi' => 'Menggabungkan teknologi informasi dengan manajemen bisnis.',
|
||||||
|
'prospek_karir' => 'IT Consultant, Systems Analyst, Project Manager'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'nama_jurusan' => 'Akuntansi',
|
||||||
|
'rumpun' => 'Soshum',
|
||||||
|
'deskripsi' => 'Mempelajari pencatatan dan pelaporan transaksi keuangan.',
|
||||||
|
'prospek_karir' => 'Auditor, Tax Consultant, Financial Analyst'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'nama_jurusan' => 'Ilmu Komunikasi',
|
||||||
|
'rumpun' => 'Soshum',
|
||||||
|
'deskripsi' => 'Mempelajari proses penyampaian pesan secara efektif.',
|
||||||
|
'prospek_karir' => 'Public Relations, Journalist, Content Creator'
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($data as $j) {
|
||||||
|
Jurusan::create($j);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Models\Jurusan;
|
||||||
|
use App\Models\ProspekKerja;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
|
class ProspekKerjaSeeder extends Seeder
|
||||||
|
{
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$semuaJurusan = [
|
||||||
|
[
|
||||||
|
'nama_jurusan' => 'Teknik Informatika / Ilmu Komputer',
|
||||||
|
'rumpun' => 'Saintek',
|
||||||
|
'deskripsi' => 'Fokus pada pengembangan perangkat lunak, kecerdasan buatan, keamanan siber, dan analisis data.',
|
||||||
|
'prospek' => [
|
||||||
|
['nama_profesi' => 'Software Engineer', 'range_gaji' => 'Rp 8.000.000 - Rp 25.000.000', 'deskripsi_pekerjaan' => 'Membangun, mengembangkan, dan memelihara aplikasi atau sistem perangkat lunak.'],
|
||||||
|
['nama_profesi' => 'Data Scientist', 'range_gaji' => 'Rp 10.000.000 - Rp 30.000.000', 'deskripsi_pekerjaan' => 'Menganalisis data besar (Big Data) untuk menemukan pola dan memberikan wawasan bagi strategi bisnis.'],
|
||||||
|
['nama_profesi' => 'Cyber Security Specialist', 'range_gaji' => 'Rp 9.000.000 - Rp 25.000.000', 'deskripsi_pekerjaan' => 'Melindungi sistem jaringan dan data perusahaan dari ancaman peretasan dan kebocoran data.'],
|
||||||
|
['nama_profesi' => 'UI/UX Designer', 'range_gaji' => 'Rp 7.000.000 - Rp 18.000.000', 'deskripsi_pekerjaan' => 'Merancang antarmuka pengguna dan pengalaman pengguna yang optimal pada website atau aplikasi.'],
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'nama_jurusan' => 'Kedokteran / Farmasi',
|
||||||
|
'rumpun' => 'Saintek',
|
||||||
|
'deskripsi' => 'Mempelajari ilmu medis, diagnosa penyakit, penanganan pasien, dan formulasi obat-obatan.',
|
||||||
|
'prospek' => [
|
||||||
|
['nama_profesi' => 'Dokter Umum / Spesialis', 'range_gaji' => 'Rp 10.000.000 - Rp 40.000.000', 'deskripsi_pekerjaan' => 'Mendiagnosis, merawat pasien, dan memberikan tindakan medis di rumah sakit atau klinik.'],
|
||||||
|
['nama_profesi' => 'Apoteker / Ahli Farmasi', 'range_gaji' => 'Rp 7.000.000 - Rp 15.000.000', 'deskripsi_pekerjaan' => 'Meracik obat, meneliti efektivitas obat baru, dan memberikan edukasi penggunaan obat kepada pasien.'],
|
||||||
|
['nama_profesi' => 'Peneliti Medis', 'range_gaji' => 'Rp 8.000.000 - Rp 20.000.000', 'deskripsi_pekerjaan' => 'Melakukan eksperimen klinis untuk menemukan vaksin atau pengobatan baru.'],
|
||||||
|
['nama_profesi' => 'Manajemen Rumah Sakit', 'range_gaji' => 'Rp 9.000.000 - Rp 25.000.000', 'deskripsi_pekerjaan' => 'Mengatur operasional, administrasi, dan kebijakan strategis di fasilitas pelayanan kesehatan.'],
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'nama_jurusan' => 'Hukum / Hubungan Internasional',
|
||||||
|
'rumpun' => 'Soshum',
|
||||||
|
'deskripsi' => 'Mempelajari sistem peradilan, regulasi negara, serta diplomasi dan politik antar negara.',
|
||||||
|
'prospek' => [
|
||||||
|
['nama_profesi' => 'Pengacara / Advokat', 'range_gaji' => 'Rp 8.000.000 - Rp 50.000.000', 'deskripsi_pekerjaan' => 'Memberikan pendampingan hukum dan mewakili klien di pengadilan.'],
|
||||||
|
['nama_profesi' => 'Hakim atau Jaksa', 'range_gaji' => 'Rp 15.000.000 - Rp 35.000.000', 'deskripsi_pekerjaan' => 'Menjalankan proses penuntutan pidana dan memberikan putusan hukum yang adil di pengadilan.'],
|
||||||
|
['nama_profesi' => 'Diplomat', 'range_gaji' => 'Rp 12.000.000 - Rp 30.000.000', 'deskripsi_pekerjaan' => 'Mewakili kepentingan negara dalam forum internasional dan menjaga hubungan bilateral.'],
|
||||||
|
['nama_profesi' => 'Legal Officer Perusahaan', 'range_gaji' => 'Rp 7.000.000 - Rp 20.000.000', 'deskripsi_pekerjaan' => 'Memastikan seluruh kegiatan operasional perusahaan berjalan sesuai dengan regulasi dan undang-undang yang berlaku.'],
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'nama_jurusan' => 'Ekonomi / Manajemen Bisnis',
|
||||||
|
'rumpun' => 'Soshum',
|
||||||
|
'deskripsi' => 'Mempelajari pengelolaan sumber daya, keuangan, strategi pemasaran, dan operasional bisnis.',
|
||||||
|
'prospek' => [
|
||||||
|
['nama_profesi' => 'Manajer Keuangan', 'range_gaji' => 'Rp 10.000.000 - Rp 30.000.000', 'deskripsi_pekerjaan' => 'Mengelola arus kas, investasi, dan menyusun perencanaan keuangan instansi atau perusahaan.'],
|
||||||
|
['nama_profesi' => 'Analis Bisnis', 'range_gaji' => 'Rp 8.000.000 - Rp 22.000.000', 'deskripsi_pekerjaan' => 'Mengevaluasi model bisnis perusahaan dan memberikan rekomendasi peningkatan efisiensi melalui data.'],
|
||||||
|
['nama_profesi' => 'Akuntan Publik', 'range_gaji' => 'Rp 7.000.000 - Rp 25.000.000', 'deskripsi_pekerjaan' => 'Melakukan audit pembukuan keuangan perusahaan agar sesuai dengan standar transparansi.'],
|
||||||
|
['nama_profesi' => 'Business Development', 'range_gaji' => 'Rp 7.000.000 - Rp 18.000.000', 'deskripsi_pekerjaan' => 'Mencari peluang kemitraan baru, klien potensial, dan strategi ekspansi pasar.'],
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'nama_jurusan' => 'Teknik Sipil / Arsitektur',
|
||||||
|
'rumpun' => 'Saintek',
|
||||||
|
'deskripsi' => 'Fokus pada perancangan, pembangunan, estetika, dan pemeliharaan infrastruktur dan bangunan.',
|
||||||
|
'prospek' => [
|
||||||
|
['nama_profesi' => 'Manajer Proyek Konstruksi', 'range_gaji' => 'Rp 10.000.000 - Rp 30.000.000', 'deskripsi_pekerjaan' => 'Mengawasi jalannya proyek konstruksi di lapangan agar selesai sesuai jadwal, kualitas, dan anggaran.'],
|
||||||
|
['nama_profesi' => 'Arsitek Profesional', 'range_gaji' => 'Rp 7.000.000 - Rp 25.000.000', 'deskripsi_pekerjaan' => 'Merancang estetika dan fungsionalitas bangunan, membuat cetak biru, serta visualisasi 3D.'],
|
||||||
|
['nama_profesi' => 'Insinyur Struktur', 'range_gaji' => 'Rp 8.000.000 - Rp 20.000.000', 'deskripsi_pekerjaan' => 'Melakukan perhitungan material untuk memastikan keamanan dan kestabilan beban bangunan atau jembatan.'],
|
||||||
|
['nama_profesi' => 'Surveyor dan Drafter', 'range_gaji' => 'Rp 5.000.000 - Rp 12.000.000', 'deskripsi_pekerjaan' => 'Mengukur kontur topografi lahan serta memindahkan sketsa desain menjadi gambar teknis presisi.'],
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'nama_jurusan' => 'Psikologi / Ilmu Sosial',
|
||||||
|
'rumpun' => 'Soshum',
|
||||||
|
'deskripsi' => 'Mempelajari perilaku manusia, proses mental, serta interaksi dan dinamika di masyarakat.',
|
||||||
|
'prospek' => [
|
||||||
|
['nama_profesi' => 'Manajer HRD (Human Resources)', 'range_gaji' => 'Rp 8.000.000 - Rp 25.000.000', 'deskripsi_pekerjaan' => 'Mengelola proses rekrutmen, pelatihan, evaluasi, dan kesejahteraan karyawan dalam suatu perusahaan.'],
|
||||||
|
['nama_profesi' => 'Psikolog Klinis / Konselor', 'range_gaji' => 'Rp 7.000.000 - Rp 20.000.000', 'deskripsi_pekerjaan' => 'Mendiagnosis dan memberikan terapi untuk gangguan kesehatan mental atau masalah emosional klien.'],
|
||||||
|
['nama_profesi' => 'Peneliti Sosial', 'range_gaji' => 'Rp 6.000.000 - Rp 15.000.000', 'deskripsi_pekerjaan' => 'Merancang dan melakukan riset tentang tren sosial, kebijakan publik, atau perilaku konsumen masyarakat.'],
|
||||||
|
['nama_profesi' => 'Konselor Pendidikan', 'range_gaji' => 'Rp 5.000.000 - Rp 12.000.000', 'deskripsi_pekerjaan' => 'Membantu siswa merencanakan masa depan akademik, memberikan motivasi, serta menangani masalah perilaku.'],
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'nama_jurusan' => 'Seni Rupa / Desain Komunikasi Visual',
|
||||||
|
'rumpun' => 'Soshum',
|
||||||
|
'deskripsi' => 'Fokus pada penyampaian pesan komunikasi melalui elemen visual, estetika rupa, dan desain kreatif.',
|
||||||
|
'prospek' => [
|
||||||
|
['nama_profesi' => 'Art Director', 'range_gaji' => 'Rp 10.000.000 - Rp 25.000.000', 'deskripsi_pekerjaan' => 'Memimpin tim kreatif dan menentukan konsep visual utama untuk kampanye iklan, film, atau majalah.'],
|
||||||
|
['nama_profesi' => 'Graphic Designer', 'range_gaji' => 'Rp 5.000.000 - Rp 15.000.000', 'deskripsi_pekerjaan' => 'Menciptakan desain identitas visual seperti logo, kemasan produk, dan materi promosi untuk brand.'],
|
||||||
|
['nama_profesi' => 'Animator / 3D Artist', 'range_gaji' => 'Rp 7.000.000 - Rp 20.000.000', 'deskripsi_pekerjaan' => 'Membuat gambar bergerak (animasi), visual effects (VFX), atau model 3D untuk industri film dan video game.'],
|
||||||
|
['nama_profesi' => 'Fotografer / Videografer Komersial', 'range_gaji' => 'Rp 6.000.000 - Rp 20.000.000', 'deskripsi_pekerjaan' => 'Mengambil dan menyunting gambar atau video untuk kebutuhan editorial, komersial, atau dokumentasi acara.'],
|
||||||
|
]
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($semuaJurusan as $data) {
|
||||||
|
$jurusan = Jurusan::firstOrCreate(
|
||||||
|
['nama_jurusan' => $data['nama_jurusan']],
|
||||||
|
[
|
||||||
|
'rumpun' => $data['rumpun'],
|
||||||
|
'deskripsi' => $data['deskripsi']
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($data['prospek'] as $prospekData) {
|
||||||
|
ProspekKerja::firstOrCreate(
|
||||||
|
[
|
||||||
|
'jurusan_id' => $jurusan->id,
|
||||||
|
'nama_profesi' => $prospekData['nama_profesi']
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'range_gaji' => $prospekData['range_gaji'],
|
||||||
|
'deskripsi_pekerjaan' => $prospekData['deskripsi_pekerjaan']
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Models\Siswa;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Jurusan;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class SiswaSeeder extends Seeder
|
||||||
|
{
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
// 1. Bersihkan data lama agar tidak terjadi error Unique NISN saat seeding ulang
|
||||||
|
// Menggunakan truncate atau delete agar ID auto-increment mulai dari awal lagi jika perlu
|
||||||
|
DB::statement('SET FOREIGN_KEY_CHECKS=0;');
|
||||||
|
Siswa::truncate();
|
||||||
|
DB::statement('SET FOREIGN_KEY_CHECKS=1;');
|
||||||
|
|
||||||
|
// 2. Ambil semua user dengan role siswa dan semua jurusan
|
||||||
|
$users = User::where('role', 'siswa')->get();
|
||||||
|
$jurusans = Jurusan::all();
|
||||||
|
|
||||||
|
// Cek jika data master (User & Jurusan) tersedia
|
||||||
|
if ($users->isEmpty() || $jurusans->isEmpty()) {
|
||||||
|
$this->command->error("Data User atau Jurusan kosong! Silahkan jalankan UserSeeder dan JurusanSeeder terlebih dahulu.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$siswaData = [
|
||||||
|
[
|
||||||
|
'nama_lengkap' => 'Galang Sefian Adji',
|
||||||
|
'nisn' => '12345678912', // Sesuaikan dengan yang ada di gambar UI Anda
|
||||||
|
'kelas' => 'XII-IPA-1',
|
||||||
|
'peminatan' => 'Saintek',
|
||||||
|
'minat_bakat' => 'Programming',
|
||||||
|
'ekonomi' => 'Mampu'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'nama_lengkap' => 'Eka Putri',
|
||||||
|
'nisn' => '1234567891',
|
||||||
|
'kelas' => 'XII-IPS-2',
|
||||||
|
'peminatan' => 'Soshum',
|
||||||
|
'minat_bakat' => 'Menulis',
|
||||||
|
'ekonomi' => 'Menengah'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'nama_lengkap' => 'Fajar Ramadhan',
|
||||||
|
'nisn' => '1234567892',
|
||||||
|
'kelas' => 'XII-IPA-2',
|
||||||
|
'peminatan' => 'Saintek',
|
||||||
|
'minat_bakat' => 'Robotik',
|
||||||
|
'ekonomi' => 'Mampu'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'nama_lengkap' => 'alex',
|
||||||
|
'nisn' => '1243254354',
|
||||||
|
'kelas' => 'XII-IPA-1',
|
||||||
|
'peminatan' => 'Saintek',
|
||||||
|
'minat_bakat' => 'Musik',
|
||||||
|
'ekonomi' => 'Mampu'
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($siswaData as $index => $data) {
|
||||||
|
if (isset($users[$index])) {
|
||||||
|
// Menggunakan Eloquent Create agar fungsi boot() di model Siswa terpanggil
|
||||||
|
// Jangan memasukkan 'id_siswa' secara manual di sini, biarkan model yang membuatnya
|
||||||
|
Siswa::create([
|
||||||
|
'user_id' => $users[$index]->id,
|
||||||
|
'jurusan_id' => $jurusans->random()->id,
|
||||||
|
'nama_lengkap' => $data['nama_lengkap'],
|
||||||
|
'nisn' => $data['nisn'],
|
||||||
|
'kelas' => $data['kelas'],
|
||||||
|
'peminatan' => $data['peminatan'],
|
||||||
|
'minat_bakat' => $data['minat_bakat'],
|
||||||
|
'ekonomi' => $data['ekonomi'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->command->info("Seeder Siswa berhasil dijalankan dengan ID otomatis.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
|
||||||
|
class UserSeeder extends Seeder
|
||||||
|
{
|
||||||
|
public function run()
|
||||||
|
{
|
||||||
|
// Membuat Admin
|
||||||
|
User::create([
|
||||||
|
'name' => 'Administrator',
|
||||||
|
'username' => 'admin',
|
||||||
|
'email' => 'admin@mail.com',
|
||||||
|
'password' => Hash::make('password123'),
|
||||||
|
'role' => 'admin', // Sesuai dengan role di AuthController
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Membuat Siswa
|
||||||
|
User::create([
|
||||||
|
'name' => 'Siswa Budi',
|
||||||
|
'username' => 'siswa01',
|
||||||
|
'email' => 'budi@mail.com',
|
||||||
|
'password' => Hash::make('password123'),
|
||||||
|
'role' => 'siswa',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,181 @@
|
||||||
|
import os
|
||||||
|
import traceback
|
||||||
|
from flask import Flask, request, jsonify
|
||||||
|
from flask_cors import CORS
|
||||||
|
import joblib
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
CORS(app)
|
||||||
|
|
||||||
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
MODEL_PATH = os.path.join(BASE_DIR, 'model_rekomendasi_15mapel.pkl')
|
||||||
|
ENCODER_PATH = os.path.join(BASE_DIR, 'encoders_15mapel.pkl')
|
||||||
|
FEATURE_COLUMNS_PATH = os.path.join(BASE_DIR, 'feature_columns_15mapel.pkl')
|
||||||
|
|
||||||
|
model = None
|
||||||
|
encoders = None
|
||||||
|
feature_columns = None
|
||||||
|
|
||||||
|
|
||||||
|
def load_artifacts():
|
||||||
|
global model, encoders, feature_columns
|
||||||
|
|
||||||
|
model = joblib.load(MODEL_PATH)
|
||||||
|
encoders = joblib.load(ENCODER_PATH)
|
||||||
|
feature_columns = joblib.load(FEATURE_COLUMNS_PATH)
|
||||||
|
|
||||||
|
if not isinstance(feature_columns, list):
|
||||||
|
raise ValueError("feature_columns_15mapel.pkl harus berisi list nama kolom.")
|
||||||
|
|
||||||
|
required_encoder_keys = ['ekonomi_orang_tua', 'rekomendasi_jurusan']
|
||||||
|
for key in required_encoder_keys:
|
||||||
|
if key not in encoders:
|
||||||
|
raise KeyError(f"Encoder '{key}' tidak ditemukan pada encoders_15mapel.pkl")
|
||||||
|
|
||||||
|
|
||||||
|
def safe_float(value, default=0.0):
|
||||||
|
try:
|
||||||
|
if value is None or value == '':
|
||||||
|
return float(default)
|
||||||
|
return float(value)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return float(default)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_required_features(data):
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
return False, "Payload JSON tidak valid."
|
||||||
|
|
||||||
|
missing = []
|
||||||
|
for col in feature_columns:
|
||||||
|
if col == 'ekonomi_orang_tua':
|
||||||
|
continue
|
||||||
|
if col not in data:
|
||||||
|
missing.append(col)
|
||||||
|
|
||||||
|
if 'ekonomi_orang_tua' not in data:
|
||||||
|
missing.append('ekonomi_orang_tua')
|
||||||
|
|
||||||
|
if missing:
|
||||||
|
return False, f"Fitur berikut belum dikirim: {', '.join(missing)}"
|
||||||
|
|
||||||
|
return True, None
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
load_artifacts()
|
||||||
|
print("✅ API Flask siap. Model, encoder, dan feature columns berhasil dimuat.")
|
||||||
|
except Exception as e:
|
||||||
|
print("❌ Gagal memuat aset machine learning.")
|
||||||
|
print(str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/', methods=['GET'])
|
||||||
|
def home():
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'message': 'Flask API Sistem Rekomendasi Jurusan aktif.'
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/health', methods=['GET'])
|
||||||
|
def health():
|
||||||
|
artifacts_ready = all([
|
||||||
|
model is not None,
|
||||||
|
encoders is not None,
|
||||||
|
feature_columns is not None
|
||||||
|
])
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': artifacts_ready,
|
||||||
|
'model_loaded': model is not None,
|
||||||
|
'encoders_loaded': encoders is not None,
|
||||||
|
'feature_columns_loaded': feature_columns is not None
|
||||||
|
}), 200 if artifacts_ready else 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/predict', methods=['POST'])
|
||||||
|
def predict():
|
||||||
|
global model, encoders, feature_columns
|
||||||
|
|
||||||
|
try:
|
||||||
|
if model is None or encoders is None or feature_columns is None:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': 'Aset model belum berhasil dimuat. Periksa file .pkl dan path-nya.'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
data = request.get_json(silent=True)
|
||||||
|
if data is None:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': 'Request harus berformat JSON.'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
is_valid, error_message = validate_required_features(data)
|
||||||
|
if not is_valid:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': error_message
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
eko_text = str(data.get('ekonomi_orang_tua', 'Mampu')).strip()
|
||||||
|
|
||||||
|
ekonomi_classes = list(encoders['ekonomi_orang_tua'].classes_)
|
||||||
|
if eko_text not in ekonomi_classes:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': f"Nilai ekonomi_orang_tua tidak valid. Gunakan salah satu: {', '.join(ekonomi_classes)}"
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
encoded_eko = encoders['ekonomi_orang_tua'].transform([eko_text])[0]
|
||||||
|
|
||||||
|
input_dict = {}
|
||||||
|
for col in feature_columns:
|
||||||
|
if col == 'ekonomi_orang_tua':
|
||||||
|
input_dict[col] = encoded_eko
|
||||||
|
else:
|
||||||
|
input_dict[col] = safe_float(data.get(col), 0)
|
||||||
|
|
||||||
|
df_input = pd.DataFrame([input_dict], columns=feature_columns)
|
||||||
|
|
||||||
|
pred_idx = model.predict(df_input)[0]
|
||||||
|
nama_jurusan = encoders['rekomendasi_jurusan'].inverse_transform([pred_idx])[0]
|
||||||
|
|
||||||
|
probabilities = None
|
||||||
|
if hasattr(model, 'predict_proba'):
|
||||||
|
try:
|
||||||
|
proba = model.predict_proba(df_input)[0]
|
||||||
|
class_labels = encoders['rekomendasi_jurusan'].inverse_transform(model.classes_)
|
||||||
|
probabilities = [
|
||||||
|
{
|
||||||
|
'jurusan': str(label),
|
||||||
|
'probabilitas': round(float(score), 4)
|
||||||
|
}
|
||||||
|
for label, score in sorted(
|
||||||
|
zip(class_labels, proba),
|
||||||
|
key=lambda x: x[1],
|
||||||
|
reverse=True
|
||||||
|
)
|
||||||
|
]
|
||||||
|
except Exception:
|
||||||
|
probabilities = None
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'prediksi_jurusan': str(nama_jurusan),
|
||||||
|
'top_predictions': probabilities
|
||||||
|
}), 200
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
traceback.print_exc()
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': f'Terjadi kesalahan saat melakukan prediksi: {str(e)}'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(host='127.0.0.1', port=5000, debug=True)
|
||||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,211 @@
|
||||||
|
/* ================================
|
||||||
|
THEME SMAN 4 JEMBER - MODERN DASHBOARD
|
||||||
|
================================ */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--primary-green: #2D7A47; /* Hijau SMAN 4 */
|
||||||
|
--dark-green: #1B4D2E;
|
||||||
|
--accent-yellow: #FFD700; /* Kuning Emas Logo */
|
||||||
|
--bg-light: #f8fafc;
|
||||||
|
--white: #ffffff;
|
||||||
|
--sidebar-width: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--bg-light);
|
||||||
|
font-family: 'Figtree', sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Layout Container */
|
||||||
|
.admin-wrapper {
|
||||||
|
display: flex;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- PERBAIKAN SIDEBAR --- */
|
||||||
|
.sidebar-modern {
|
||||||
|
width: var(--sidebar-width);
|
||||||
|
background-color: var(--primary-green);
|
||||||
|
color: var(--white);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: fixed;
|
||||||
|
height: 100vh; /* Mengunci tinggi sidebar sesuai tinggi layar */
|
||||||
|
box-shadow: 4px 0 10px rgba(0,0,0,0.1);
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header {
|
||||||
|
padding: 30px 20px;
|
||||||
|
text-align: center;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||||
|
flex-shrink: 0; /* Header tidak akan mengecil saat di-scroll */
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header img {
|
||||||
|
width: 60px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Area Menu dengan Fitur Scroll */
|
||||||
|
.sidebar-menu {
|
||||||
|
padding: 20px 15px;
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow-y: auto; /* Mengaktifkan scroll vertikal */
|
||||||
|
scrollbar-width: thin; /* Untuk Firefox */
|
||||||
|
scrollbar-color: rgba(255,255,255,0.2) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Scrollbar untuk Chrome/Edge/Safari */
|
||||||
|
.sidebar-menu::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-menu::-webkit-scrollbar-thumb {
|
||||||
|
background-color: rgba(255,255,255,0.2);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-menu::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-label {
|
||||||
|
font-size: 11px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 700;
|
||||||
|
color: rgba(255,255,255,0.5);
|
||||||
|
margin: 20px 0 10px 10px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 15px;
|
||||||
|
color: var(--white);
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item i {
|
||||||
|
width: 25px;
|
||||||
|
font-size: 18px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:hover {
|
||||||
|
background-color: rgba(255,255,255,0.1);
|
||||||
|
color: var(--accent-yellow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.active {
|
||||||
|
background-color: var(--white);
|
||||||
|
color: var(--primary-green);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Content Area */
|
||||||
|
.main-content {
|
||||||
|
margin-left: var(--sidebar-width);
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 0; /* Mencegah overflow pada elemen flex */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Topbar */
|
||||||
|
.topbar {
|
||||||
|
height: 70px;
|
||||||
|
background: var(--white);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 30px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar {
|
||||||
|
background: #f1f5f9;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 8px 15px;
|
||||||
|
width: 350px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar input {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
margin-left: 10px;
|
||||||
|
outline: none;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stats Cards */
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||||
|
gap: 25px;
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: var(--white);
|
||||||
|
padding: 25px;
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0,0,0,0.02);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
border: 1px solid #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-info h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-info p {
|
||||||
|
margin: 0;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
width: 55px;
|
||||||
|
height: 55px;
|
||||||
|
border-radius: 15px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-siswa { background: #eef2ff; color: #4f46e5; }
|
||||||
|
.icon-rekom { background: #ecfdf5; color: var(--primary-green); }
|
||||||
|
.icon-akurasi { background: #faf5ff; color: #9333ea; }
|
||||||
|
|
||||||
|
/* Dashboard Section */
|
||||||
|
.dashboard-section {
|
||||||
|
padding: 0 30px 30px 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
background: var(--white);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 25px;
|
||||||
|
border: 1px solid #f1f5f9;
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;700;800&display=swap');
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--asgard-green-dark: #064e3b;
|
||||||
|
--asgard-green-bright: #10b981;
|
||||||
|
--asgard-lime: #a3e635;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.auth-page {
|
||||||
|
background-color: #f0fdf4;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-family: 'Plus Jakarta Sans', sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Container Utama Split Screen */
|
||||||
|
.login-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1100px;
|
||||||
|
height: 700px;
|
||||||
|
background: white;
|
||||||
|
display: flex;
|
||||||
|
border-radius: 3rem;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 50px 100px -20px rgba(6, 78, 59, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sisi Kiri: Visual Identity */
|
||||||
|
.login-visual {
|
||||||
|
flex: 1;
|
||||||
|
background: linear-gradient(135deg, var(--asgard-green-dark) 0%, #065f46 100%);
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 60px;
|
||||||
|
position: relative;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.login-visual { display: flex; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.visual-pattern {
|
||||||
|
position: absolute;
|
||||||
|
top: 0; left: 0; width: 100%; height: 100%;
|
||||||
|
opacity: 0.1;
|
||||||
|
background-image: radial-gradient(#fff 1px, transparent 1px);
|
||||||
|
background-size: 30px 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sisi Kanan: Form */
|
||||||
|
.login-form-area {
|
||||||
|
flex: 1;
|
||||||
|
padding: 60px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-modern {
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 2px solid #f1f5f9;
|
||||||
|
border-radius: 1.2rem;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-modern:focus {
|
||||||
|
background: white;
|
||||||
|
border-color: var(--asgard-green-bright);
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-login {
|
||||||
|
background: var(--asgard-green-dark);
|
||||||
|
color: white;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 1.2rem;
|
||||||
|
font-weight: 800;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-login:hover {
|
||||||
|
background: #000;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 10px 20px rgba(6, 78, 59, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-ai {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 50px;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,155 @@
|
||||||
|
|
||||||
|
.table-container-modern {
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: #ffffff;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-modern {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-modern thead th {
|
||||||
|
background-color: #f8fafc;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 11px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-bottom: 2px solid #e2e8f0;
|
||||||
|
border-right: 1px solid #e2e8f0; /* Garis antar kolom */
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-modern thead th:last-child {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-modern tbody td {
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-bottom: 1px solid #f1f5f9;
|
||||||
|
vertical-align: middle;
|
||||||
|
border-right: 1px solid #f1f5f9; /* Garis antar kolom */
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-modern tbody td:last-child {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 2. Opsi Tombol Aksi (Edit & Hapus) */
|
||||||
|
.opsi-wrapper {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-action {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: none;
|
||||||
|
color: #ffffff;
|
||||||
|
transition: transform 0.2s, opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-action:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-edit { background-color: #4f46e5; }
|
||||||
|
.btn-delete { background-color: #ef4444; }
|
||||||
|
|
||||||
|
/* 3. Pagination (Menyatu di bawah tabel) */
|
||||||
|
.pagination-container {
|
||||||
|
padding: 15px 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-top: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-link-custom {
|
||||||
|
padding: 6px 14px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-link-custom.active {
|
||||||
|
background-color: #007bff;
|
||||||
|
color: #ffffff;
|
||||||
|
border-color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-link-custom:disabled {
|
||||||
|
background-color: #f8fafc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 4. Form Styling (Satu Baris Satu Input) */
|
||||||
|
.form-group-custom {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group-custom label {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #475569;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-custom {
|
||||||
|
border-radius: 10px !important;
|
||||||
|
border: 1px solid #e2e8f0 !important;
|
||||||
|
padding: 12px 15px !important;
|
||||||
|
font-size: 14px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-custom:focus {
|
||||||
|
border-color: var(--primary-green) !important;
|
||||||
|
box-shadow: 0 0 0 3px rgba(45, 122, 71, 0.1) !important;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Badge Rumpun Jurusan */
|
||||||
|
.badge-saintek {
|
||||||
|
background-color: #e0f2fe; /* Warna Biru Muda */
|
||||||
|
color: #0369a1; /* Warna Biru Tua */
|
||||||
|
border: 1px solid #bae6fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-soshum {
|
||||||
|
background-color: #fef3c7; /* Warna Kuning Muda */
|
||||||
|
color: #92400e; /* Warna Cokelat/Oren */
|
||||||
|
border: 1px solid #fde68a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Base style untuk badge jika belum ada */
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.35em 0.65em;
|
||||||
|
font-size: 0.75em;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
vertical-align: baseline;
|
||||||
|
border-radius: 50rem;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap');
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--asgard-green: #064e3b;
|
||||||
|
--asgard-lime: #10b981;
|
||||||
|
--asgard-yellow: #fbbf24;
|
||||||
|
}
|
||||||
|
|
||||||
|
html { scroll-behavior: smooth; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Plus Jakarta Sans', sans-serif;
|
||||||
|
/* Background Modern Gradasi Hijau - Kuning */
|
||||||
|
background:
|
||||||
|
radial-gradient(at 0% 0%, rgba(16, 185, 129, 0.15) 0px, transparent 50%),
|
||||||
|
radial-gradient(at 100% 100%, rgba(251, 191, 36, 0.1) 0px, transparent 50%),
|
||||||
|
linear-gradient(180deg, #ffffff 0%, #f0fdf4 100%);
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bento Card Glassmorphism */
|
||||||
|
.bento-card {
|
||||||
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||||
|
border-radius: 2.5rem;
|
||||||
|
transition: all 0.5s cubic-bezier(0.23, 1, 0.32, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bento-card:hover {
|
||||||
|
transform: translateY(-12px);
|
||||||
|
box-shadow: 0 30px 60px rgba(6, 78, 59, 0.12);
|
||||||
|
border-color: var(--asgard-lime);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Swiper Slider Styling */
|
||||||
|
.swiper-pagination-bullet-active {
|
||||||
|
background: var(--asgard-green) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-gradient {
|
||||||
|
background: linear-gradient(135deg, var(--asgard-green) 0%, var(--asgard-lime) 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer Dark Smapa */
|
||||||
|
.footer-asgard {
|
||||||
|
background: #022c22;
|
||||||
|
color: #ecfdf5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Interactive Floating Animation */
|
||||||
|
.animate-float {
|
||||||
|
animation: float 6s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% { transform: translateY(0); }
|
||||||
|
50% { transform: translateY(-20px); }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;700;800&display=swap');
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--asgard-dark: #064e3b;
|
||||||
|
--asgard-lime: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
body { font-family: 'Plus Jakarta Sans', sans-serif; }
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
@apply transition-all duration-300 flex items-center gap-4 p-4 rounded-2xl text-slate-400 text-sm tracking-tight;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link.active {
|
||||||
|
background: #f0fdf4;
|
||||||
|
color: var(--asgard-dark);
|
||||||
|
@apply shadow-sm border border-emerald-50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link:hover:not(.active) {
|
||||||
|
@apply bg-slate-50 text-emerald-600 translate-x-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar { background: white; border-right: 1px solid #f1f5f9; }
|
||||||
|
|
||||||
|
input:focus, select:focus {
|
||||||
|
@apply shadow-lg shadow-emerald-50;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,144 @@
|
||||||
|
@extends('admin.layout')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<link rel="stylesheet" href="{{ asset('css/fitur-admin.css') }}">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
|
|
||||||
|
<div class="dashboard-section">
|
||||||
|
<div class="mb-4 d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<h4 class="fw-bold text-dark">Manajemen Data Alumni</h4>
|
||||||
|
<p class="text-muted small">Kelola data historis lulusan untuk kebutuhan dataset sistem prediksi.</p>
|
||||||
|
</div>
|
||||||
|
<a href="{{ route('admin.alumni.create') }}" class="btn text-white px-4 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">
|
||||||
|
<i class="fas fa-plus me-2"></i>Tambah Data Alumni
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-container-modern shadow-sm">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-modern mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="text-center" width="100">ID Alumni</th>
|
||||||
|
<th>Nama Alumni</th>
|
||||||
|
<th>Perguruan Tinggi</th>
|
||||||
|
<th class="text-center">Jurusan Diambil</th>
|
||||||
|
<th class="text-center" width="150">Opsi</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="alumni-table-body">
|
||||||
|
<tr><td colspan="5" class="text-center py-5 text-muted">Memuat data alumni...</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pagination-container">
|
||||||
|
<div class="text-muted small" id="pagination-info">
|
||||||
|
Menampilkan 0 sampai 0 dari 0 data
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-1" id="pagination-controls">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
async function fetchAlumni(page = 1) {
|
||||||
|
const token = localStorage.getItem('access_token');
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/admin/alumni?page=${page}`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer ' + token,
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
const tbody = document.getElementById('alumni-table-body');
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
|
||||||
|
if(!result.data || result.data.length === 0) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="5" class="text-center py-4">Tidak ada data alumni.</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.data.forEach((item) => {
|
||||||
|
tbody.innerHTML += `
|
||||||
|
<tr>
|
||||||
|
<td class="text-center fw-bold text-muted">${item.id_alumni}</td>
|
||||||
|
<td class="fw-bold text-dark">${item.nama_alumni}</td>
|
||||||
|
<td class="text-muted small">${item.perguruan_tinggi}</td>
|
||||||
|
<td class="text-center"><span class="badge bg-light text-dark border px-3 py-2" style="border-radius:8px">${item.jurusan_diambil}</span></td>
|
||||||
|
<td class="text-center">
|
||||||
|
<div class="opsi-wrapper">
|
||||||
|
<a href="/admin/alumni/${item.id}/edit" class="btn-action btn-edit" title="Edit">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
<button onclick="deleteAlumni(${item.id})" class="btn-action btn-delete" title="Hapus">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('pagination-info').innerText =
|
||||||
|
`Menampilkan ${result.from || 0} sampai ${result.to || 0} dari ${result.total || 0} data`;
|
||||||
|
|
||||||
|
renderPagination(result);
|
||||||
|
} catch (error) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="5" class="text-center text-danger">Gagal memuat data.</td></tr>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPagination(data) {
|
||||||
|
const controls = document.getElementById('pagination-controls');
|
||||||
|
controls.innerHTML = '';
|
||||||
|
|
||||||
|
const prevBtn = document.createElement('button');
|
||||||
|
prevBtn.className = 'page-link-custom';
|
||||||
|
prevBtn.innerHTML = '« Previous';
|
||||||
|
prevBtn.disabled = data.current_page === 1;
|
||||||
|
prevBtn.onclick = () => fetchAlumni(data.current_page - 1);
|
||||||
|
controls.appendChild(prevBtn);
|
||||||
|
|
||||||
|
const pageBtn = document.createElement('button');
|
||||||
|
pageBtn.className = 'page-link-custom active';
|
||||||
|
pageBtn.innerText = data.current_page;
|
||||||
|
controls.appendChild(pageBtn);
|
||||||
|
|
||||||
|
const nextBtn = document.createElement('button');
|
||||||
|
nextBtn.className = 'page-link-custom';
|
||||||
|
nextBtn.innerHTML = 'Next »';
|
||||||
|
nextBtn.disabled = data.current_page === data.last_page;
|
||||||
|
nextBtn.onclick = () => fetchAlumni(data.current_page + 1);
|
||||||
|
controls.appendChild(nextBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteAlumni(id) {
|
||||||
|
const res = await Swal.fire({
|
||||||
|
title: 'Hapus Data?',
|
||||||
|
text: "Data alumni akan dihapus secara permanen!",
|
||||||
|
icon: 'warning',
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonColor: '#ef4444',
|
||||||
|
confirmButtonText: 'Ya, Hapus'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.isConfirmed) {
|
||||||
|
const token = localStorage.getItem('access_token');
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/admin/alumni/' + id, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { 'Authorization': 'Bearer ' + token }
|
||||||
|
});
|
||||||
|
if(response.ok) {
|
||||||
|
Swal.fire('Terhapus!', 'Data berhasil dihapus.', 'success');
|
||||||
|
fetchAlumni();
|
||||||
|
}
|
||||||
|
} catch (error) { Swal.fire('Error!', 'Koneksi gagal.', 'error'); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener('DOMContentLoaded', () => fetchAlumni(1));
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
@extends('admin.layout')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<link rel="stylesheet" href="{{ asset('css/fitur-admin.css') }}">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
|
|
||||||
|
<div class="dashboard-section d-flex justify-content-center">
|
||||||
|
<div style="width: 100%; max-width: 700px;">
|
||||||
|
<div class="mb-4 text-center">
|
||||||
|
<h4 class="fw-bold text-dark">Tambah Data Alumni</h4>
|
||||||
|
<p class="text-muted small">Input data historis untuk memperkuat akurasi algoritma Random Forest.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card border-0 shadow-sm rounded-4">
|
||||||
|
<div class="card-body p-5">
|
||||||
|
<form id="formCreateAlumni">
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-md-12 form-group-custom">
|
||||||
|
<label>Nama Lengkap Alumni</label>
|
||||||
|
<input type="text" id="nama_alumni" class="form-control input-custom" placeholder="Contoh: Ahmad Fauzi" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-12 form-group-custom">
|
||||||
|
<label>Perguruan Tinggi</label>
|
||||||
|
<input type="text" id="perguruan_tinggi" class="form-control input-custom" placeholder="Contoh: Universitas Indonesia" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-12 form-group-custom">
|
||||||
|
<label>Jurusan Kuliah yang Diambil</label>
|
||||||
|
<input type="text" id="jurusan_diambil" class="form-control input-custom" placeholder="Contoh: Teknik Informatika" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-5 d-flex gap-2 justify-content-end">
|
||||||
|
<button type="submit" id="btnSave" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">Simpan Alumni</button>
|
||||||
|
<a href="{{ route('admin.alumni') }}" class="btn btn-light px-4" style="border-radius: 10px;">Batal</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.getElementById('formCreateAlumni').addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const btn = document.getElementById('btnSave');
|
||||||
|
btn.disabled = true;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
nama_alumni: document.getElementById('nama_alumni').value,
|
||||||
|
perguruan_tinggi: document.getElementById('perguruan_tinggi').value,
|
||||||
|
jurusan_diambil: document.getElementById('jurusan_diambil').value
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/admin/alumni', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': 'Bearer ' + localStorage.getItem('access_token'),
|
||||||
|
'Accept': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
Swal.fire({ icon: 'success', title: 'Berhasil!', confirmButtonColor: '#2D6A4F' })
|
||||||
|
.then(() => { window.location.href = "{{ route('admin.alumni') }}"; });
|
||||||
|
}
|
||||||
|
} catch (error) { Swal.fire('Error!', 'Gagal menghubungi server.', 'error'); }
|
||||||
|
finally { btn.disabled = false; }
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
@extends('admin.layout')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<link rel="stylesheet" href="{{ asset('css/fitur-admin.css') }}">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
|
|
||||||
|
<div class="dashboard-section d-flex justify-content-center">
|
||||||
|
<div style="width: 100%; max-width: 700px;">
|
||||||
|
<div class="mb-4 text-center">
|
||||||
|
<h4 class="fw-bold text-dark">Edit Data Alumni</h4>
|
||||||
|
<p id="display_id_alumni" class="text-muted small fw-bold">Memuat data...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card border-0 shadow-sm rounded-4">
|
||||||
|
<div class="card-body p-5">
|
||||||
|
<form id="formEditAlumni">
|
||||||
|
<input type="hidden" id="alumni_id_db" value="{{ $id }}">
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-md-12 form-group-custom">
|
||||||
|
<label>Nama Lengkap Alumni</label>
|
||||||
|
<input type="text" id="nama_alumni" class="form-control input-custom" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-12 form-group-custom">
|
||||||
|
<label>Perguruan Tinggi</label>
|
||||||
|
<input type="text" id="perguruan_tinggi" class="form-control input-custom" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-12 form-group-custom">
|
||||||
|
<label>Jurusan Kuliah</label>
|
||||||
|
<input type="text" id="jurusan_diambil" class="form-control input-custom" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-5 d-flex gap-2 justify-content-end">
|
||||||
|
<button type="submit" id="btnUpdate" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">Perbarui Data</button>
|
||||||
|
<a href="{{ route('admin.alumni') }}" class="btn btn-light px-4" style="border-radius: 10px;">Batal</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const dbId = document.getElementById('alumni_id_db').value;
|
||||||
|
|
||||||
|
async function loadAlumni() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/admin/alumni/${dbId}`, {
|
||||||
|
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('access_token') }
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
if (response.ok) {
|
||||||
|
document.getElementById('nama_alumni').value = result.data.nama_alumni;
|
||||||
|
document.getElementById('perguruan_tinggi').value = result.data.perguruan_tinggi;
|
||||||
|
document.getElementById('jurusan_diambil').value = result.data.jurusan_diambil;
|
||||||
|
document.getElementById('display_id_alumni').innerText = `ID Alumni: ${result.data.id_alumni}`;
|
||||||
|
}
|
||||||
|
} catch (error) { console.error(error); }
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('formEditAlumni').addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const btn = document.getElementById('btnUpdate');
|
||||||
|
btn.disabled = true;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
nama_alumni: document.getElementById('nama_alumni').value,
|
||||||
|
perguruan_tinggi: document.getElementById('perguruan_tinggi').value,
|
||||||
|
jurusan_diambil: document.getElementById('jurusan_diambil').value
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/admin/alumni/${dbId}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': 'Bearer ' + localStorage.getItem('access_token'),
|
||||||
|
'Accept': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
Swal.fire({ icon: 'success', title: 'Berhasil Diperbarui!', confirmButtonColor: '#2D6A4F' })
|
||||||
|
.then(() => { window.location.href = "{{ route('admin.alumni') }}"; });
|
||||||
|
}
|
||||||
|
} catch (error) { Swal.fire('Error!', 'Gagal update.', 'error'); }
|
||||||
|
finally { btn.disabled = false; }
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', loadAlumni);
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
|
|
@ -0,0 +1,138 @@
|
||||||
|
@extends('admin.layout')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="mb-4">
|
||||||
|
<h4 class="fw-bold text-dark">Overview Statistik Aplikasi</h4>
|
||||||
|
<p class="text-muted small">Selamat datang di panel kendali sistem rekomendasi jurusan SMAN 4 Jember.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-info">
|
||||||
|
<p>Total Siswa</p>
|
||||||
|
<h3 id="stat-total-siswa">0</h3>
|
||||||
|
</div>
|
||||||
|
<div class="stat-icon icon-siswa">
|
||||||
|
<i class="fas fa-user-graduate"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-info">
|
||||||
|
<p>Rekomendasi Diproses</p>
|
||||||
|
<h3 id="stat-total-rekomendasi">0</h3>
|
||||||
|
</div>
|
||||||
|
<div class="stat-icon icon-rekom">
|
||||||
|
<i class="fas fa-check-circle"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-info">
|
||||||
|
<p>Rata-rata Akurasi</p>
|
||||||
|
<h3 id="stat-rata-akurasi">0%</h3>
|
||||||
|
</div>
|
||||||
|
<div class="stat-icon icon-akurasi">
|
||||||
|
<i class="fas fa-chart-line"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dashboard-section mt-4">
|
||||||
|
<div class="chart-container">
|
||||||
|
<h5 class="fw-bold mb-4 d-flex align-items-center gap-2">
|
||||||
|
<div style="width: 4px; height: 20px; background: var(--primary-green); border-radius: 10px;"></div>
|
||||||
|
Distribusi Rekomendasi Jurusan SMAN 4 Jember
|
||||||
|
</h5>
|
||||||
|
|
||||||
|
<div id="chart-wrapper" style="height: 350px; display: flex; align-items: center; justify-content: center; background: #f8fafc; border-radius: 15px; border: 2px dashed #e2e8f0; padding: 20px;">
|
||||||
|
<canvas id="distribusiChart" class="d-none w-100 h-100"></canvas>
|
||||||
|
<div id="chart-placeholder" class="text-center">
|
||||||
|
<i class="fas fa-chart-pie fa-3x text-light mb-3"></i>
|
||||||
|
<p class="text-muted">Memuat grafik distribusi...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@push('scripts')
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', async function() {
|
||||||
|
const token = localStorage.getItem('access_token');
|
||||||
|
if (!token) {
|
||||||
|
window.location.href = '/login';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/admin/dashboard-stats', {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
const data = result.data;
|
||||||
|
|
||||||
|
document.getElementById('stat-total-siswa').innerText = data.total_siswa;
|
||||||
|
document.getElementById('stat-total-rekomendasi').innerText = data.rekomendasi_diproses;
|
||||||
|
document.getElementById('stat-rata-akurasi').innerText = data.rata_rata_akurasi + '%';
|
||||||
|
|
||||||
|
const distribusi = data.distribusi_jurusan;
|
||||||
|
const placeholder = document.getElementById('chart-placeholder');
|
||||||
|
const canvas = document.getElementById('distribusiChart');
|
||||||
|
const wrapper = document.getElementById('chart-wrapper');
|
||||||
|
|
||||||
|
if (distribusi && distribusi.length > 0) {
|
||||||
|
placeholder.classList.add('d-none');
|
||||||
|
canvas.classList.remove('d-none');
|
||||||
|
wrapper.style.border = 'none';
|
||||||
|
wrapper.style.background = 'transparent';
|
||||||
|
|
||||||
|
const labels = distribusi.map(item => item.jurusan);
|
||||||
|
const counts = distribusi.map(item => item.total);
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
new Chart(ctx, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: labels,
|
||||||
|
datasets: [{
|
||||||
|
label: 'Jumlah Rekomendasi',
|
||||||
|
data: counts,
|
||||||
|
backgroundColor: 'rgba(45, 122, 71, 0.8)',
|
||||||
|
borderColor: 'rgba(45, 122, 71, 1)',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 5
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
ticks: { stepSize: 1 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: { display: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
placeholder.querySelector('p').innerText = 'Belum ada data riwayat rekomendasi.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Gagal memuat data dashboard:', error);
|
||||||
|
document.getElementById('chart-placeholder').querySelector('p').innerText = 'Gagal memuat data.';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
|
@endsection
|
||||||
|
|
@ -0,0 +1,145 @@
|
||||||
|
@extends('admin.layout')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<link rel="stylesheet" href="{{ asset('css/fitur-admin.css') }}">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
|
|
||||||
|
<div class="dashboard-section">
|
||||||
|
<div class="mb-4 d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<h4 class="fw-bold text-dark">Data Guru BK</h4>
|
||||||
|
<p class="text-muted small">Kelola daftar administrator dan guru pembimbing SMAN 4 Jember.</p>
|
||||||
|
</div>
|
||||||
|
<a href="{{ route('admin.guru.create') }}" class="btn text-white px-4 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">
|
||||||
|
<i class="fas fa-plus me-2"></i>Tambah Guru BK
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-container-modern shadow-sm">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-modern mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="text-center" width="100">ID Guru</th>
|
||||||
|
<th>Nama Lengkap & Gelar</th>
|
||||||
|
<th class="text-center" width="150">Opsi</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="guru-table-body">
|
||||||
|
<tr><td colspan="3" class="text-center py-5 text-muted">Memuat data...</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pagination-container">
|
||||||
|
<div class="text-muted small" id="pagination-info">
|
||||||
|
Menampilkan 0 sampai 0 dari 0 data
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-1" id="pagination-controls">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
async function fetchGuru(page = 1) {
|
||||||
|
const token = localStorage.getItem('access_token');
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/admin/guru?page=${page}`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer ' + token,
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
const tbody = document.getElementById('guru-table-body');
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
|
||||||
|
if(!result.data || result.data.length === 0) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="3" class="text-center py-4">Tidak ada data guru.</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.data.forEach((item) => {
|
||||||
|
tbody.innerHTML += `
|
||||||
|
<tr>
|
||||||
|
<td class="text-center fw-bold text-muted">${item.id_guru}</td>
|
||||||
|
<td>
|
||||||
|
<div class="d-flex align-items-center gap-3">
|
||||||
|
<span class="badge badge-bk rounded-circle d-flex align-items-center justify-content-center" style="width: 30px; height: 30px;">BK</span>
|
||||||
|
<b class="text-dark">${item.nama_guru}</b>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<div class="opsi-wrapper">
|
||||||
|
<a href="/admin/guru/${item.id}/edit" class="btn-action btn-edit" title="Edit">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
<button onclick="deleteGuru(${item.id})" class="btn-action btn-delete" title="Hapus">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('pagination-info').innerText =
|
||||||
|
`Menampilkan ${result.from || 0} sampai ${result.to || 0} dari ${result.total || 0} data`;
|
||||||
|
|
||||||
|
renderPagination(result);
|
||||||
|
} catch (error) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="3" class="text-center text-danger">Gagal memuat data.</td></tr>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPagination(data) {
|
||||||
|
const controls = document.getElementById('pagination-controls');
|
||||||
|
controls.innerHTML = '';
|
||||||
|
|
||||||
|
const prevBtn = document.createElement('button');
|
||||||
|
prevBtn.className = 'page-link-custom';
|
||||||
|
prevBtn.innerHTML = '« Previous';
|
||||||
|
prevBtn.disabled = data.current_page === 1;
|
||||||
|
prevBtn.onclick = () => fetchGuru(data.current_page - 1);
|
||||||
|
controls.appendChild(prevBtn);
|
||||||
|
|
||||||
|
const pageBtn = document.createElement('button');
|
||||||
|
pageBtn.className = 'page-link-custom active';
|
||||||
|
pageBtn.innerText = data.current_page || 1;
|
||||||
|
controls.appendChild(pageBtn);
|
||||||
|
|
||||||
|
const nextBtn = document.createElement('button');
|
||||||
|
nextBtn.className = 'page-link-custom';
|
||||||
|
nextBtn.innerHTML = 'Next »';
|
||||||
|
nextBtn.disabled = data.current_page === data.last_page;
|
||||||
|
nextBtn.onclick = () => fetchGuru(data.current_page + 1);
|
||||||
|
controls.appendChild(nextBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteGuru(id) {
|
||||||
|
const res = await Swal.fire({
|
||||||
|
title: 'Hapus Guru?',
|
||||||
|
text: "Data guru akan dihapus permanen!",
|
||||||
|
icon: 'warning',
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonColor: '#ef4444',
|
||||||
|
confirmButtonText: 'Ya, Hapus'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.isConfirmed) {
|
||||||
|
const token = localStorage.getItem('access_token');
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/admin/guru/' + id, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { 'Authorization': 'Bearer ' + token }
|
||||||
|
});
|
||||||
|
if(response.ok) {
|
||||||
|
Swal.fire('Terhapus!', 'Data guru berhasil dihapus.', 'success');
|
||||||
|
fetchGuru();
|
||||||
|
}
|
||||||
|
} catch (error) { Swal.fire('Error!', 'Koneksi gagal.', 'error'); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener('DOMContentLoaded', () => fetchGuru(1));
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
@extends('admin.layout')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<link rel="stylesheet" href="{{ asset('css/fitur-admin.css') }}">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
|
|
||||||
|
<div class="dashboard-section d-flex justify-content-center">
|
||||||
|
<div style="width: 100%; max-width: 700px;">
|
||||||
|
<div class="mb-4 text-center">
|
||||||
|
<h4 class="fw-bold text-dark">Tambah Guru BK</h4>
|
||||||
|
<p class="text-muted small">Daftarkan akun guru pembimbing baru untuk sistem rekomendasi.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card border-0 shadow-sm rounded-4">
|
||||||
|
<div class="card-body p-5">
|
||||||
|
<form id="formCreateGuru">
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-md-12 form-group-custom">
|
||||||
|
<label class="fw-bold small text-muted">Nama Lengkap & Gelar</label>
|
||||||
|
<input type="text" id="nama_guru" class="form-control input-custom" placeholder="Contoh: Budi Santoso, S.Pd" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-5 d-flex gap-2 justify-content-end">
|
||||||
|
<button type="submit" id="btnSave" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">Daftarkan Guru</button>
|
||||||
|
<a href="{{ route('admin.guru') }}" class="btn btn-light px-4" style="border-radius: 10px;">Batal</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.getElementById('formCreateGuru').addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const btn = document.getElementById('btnSave');
|
||||||
|
btn.disabled = true;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
nama_guru: document.getElementById('nama_guru').value
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/admin/guru', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': 'Bearer ' + localStorage.getItem('access_token'),
|
||||||
|
'Accept': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
Swal.fire({ icon: 'success', title: 'Berhasil!', confirmButtonColor: '#2D6A4F' })
|
||||||
|
.then(() => { window.location.href = "{{ route('admin.guru') }}"; });
|
||||||
|
}
|
||||||
|
} catch (error) { Swal.fire('Error!', 'Gagal menghubungi server.', 'error'); }
|
||||||
|
finally { btn.disabled = false; }
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
@extends('admin.layout')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<link rel="stylesheet" href="{{ asset('css/fitur-admin.css') }}">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
|
|
||||||
|
<div class="dashboard-section d-flex justify-content-center">
|
||||||
|
<div style="width: 100%; max-width: 700px;">
|
||||||
|
<div class="mb-4 text-center">
|
||||||
|
<h4 class="fw-bold text-dark">Edit Data Guru</h4>
|
||||||
|
<p id="display_id_guru" class="text-muted small fw-bold">Memuat profil...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card border-0 shadow-sm rounded-4">
|
||||||
|
<div class="card-body p-5">
|
||||||
|
<form id="formEditGuru">
|
||||||
|
<input type="hidden" id="guru_id_db" value="{{ $id }}">
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-md-12 form-group-custom">
|
||||||
|
<label class="fw-bold small text-muted">Nama Lengkap & Gelar</label>
|
||||||
|
<input type="text" id="nama_guru" class="form-control input-custom" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-5 d-flex gap-2 justify-content-end">
|
||||||
|
<button type="submit" id="btnUpdate" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">Update Profil</button>
|
||||||
|
<a href="{{ route('admin.guru') }}" class="btn btn-light px-4" style="border-radius: 10px;">Batal</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const dbId = document.getElementById('guru_id_db').value;
|
||||||
|
|
||||||
|
async function loadGuru() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/admin/guru/${dbId}`, {
|
||||||
|
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('access_token') }
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
if (response.ok) {
|
||||||
|
document.getElementById('nama_guru').value = result.data.nama_guru;
|
||||||
|
document.getElementById('display_id_guru').innerText = `Ubah profil administrator guru BK (${result.data.id_guru}).`;
|
||||||
|
}
|
||||||
|
} catch (error) { console.error(error); }
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('formEditGuru').addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const btn = document.getElementById('btnUpdate');
|
||||||
|
btn.disabled = true;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
nama_guru: document.getElementById('nama_guru').value
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/admin/guru/${dbId}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': 'Bearer ' + localStorage.getItem('access_token'),
|
||||||
|
'Accept': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
Swal.fire({ icon: 'success', title: 'Berhasil Diperbarui!', confirmButtonColor: '#2D6A4F' })
|
||||||
|
.then(() => { window.location.href = "{{ route('admin.guru') }}"; });
|
||||||
|
}
|
||||||
|
} catch (error) { Swal.fire('Error!', 'Gagal koneksi.', 'error'); }
|
||||||
|
finally { btn.disabled = false; }
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', loadGuru);
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
|
|
@ -0,0 +1,153 @@
|
||||||
|
@extends('admin.layout')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<link rel="stylesheet" href="{{ asset('css/fitur-admin.css') }}">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
|
|
||||||
|
<div class="dashboard-section">
|
||||||
|
<div class="mb-4 d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<h4 class="fw-bold text-dark">Data Master Jurusan</h4>
|
||||||
|
<p class="text-muted small">Kelola daftar jurusan kuliah yang tersedia dalam sistem prediksi.</p>
|
||||||
|
</div>
|
||||||
|
<a href="{{ route('admin.jurusan.create') }}" class="btn text-white px-4 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">
|
||||||
|
<i class="fas fa-plus me-2"></i>Tambah Jurusan
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-container-modern shadow-sm">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-modern mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="text-center" width="60">No</th>
|
||||||
|
<th>Nama Jurusan</th>
|
||||||
|
<th class="text-center">Rumpun</th>
|
||||||
|
<th>Prospek Karir</th>
|
||||||
|
<th class="text-center" width="150">Opsi</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="jurusan-table-body">
|
||||||
|
<tr><td colspan="5" class="text-center py-5 text-muted">Memuat data...</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pagination-container">
|
||||||
|
<div class="text-muted small" id="pagination-info">
|
||||||
|
Menampilkan 0 sampai 0 dari 0 data
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-1" id="pagination-controls">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
async function fetchJurusan(page = 1) {
|
||||||
|
const token = localStorage.getItem('access_token');
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/admin/jurusan?page=${page}`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer ' + token,
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
const tbody = document.getElementById('jurusan-table-body');
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
|
||||||
|
if(!result.data || result.data.length === 0) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="5" class="text-center py-4">Tidak ada data jurusan.</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.data.forEach((item, index) => {
|
||||||
|
|
||||||
|
const rumpunClass = item.rumpun.toLowerCase() === 'saintek' ? 'badge-saintek' : 'badge-soshum';
|
||||||
|
|
||||||
|
const row = `
|
||||||
|
<tr>
|
||||||
|
<td class="text-center text-muted">${(result.current_page - 1) * result.per_page + (index + 1)}</td>
|
||||||
|
<td class="fw-bold text-dark text-uppercase">${item.nama_jurusan}</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<span class="badge rounded-pill px-3 py-2 ${rumpunClass}">
|
||||||
|
${item.rumpun}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="small text-muted">${item.prospek_karir || '-'}</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<div class="opsi-wrapper">
|
||||||
|
<a href="/admin/jurusan/${item.id}/edit" class="btn-action btn-edit" title="Edit">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
<button onclick="deleteJurusan(${item.id})" class="btn-action btn-delete" title="Hapus">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
tbody.innerHTML += row;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('pagination-info').innerText =
|
||||||
|
`Menampilkan ${result.from || 0} sampai ${result.to || 0} dari ${result.total || 0} data`;
|
||||||
|
|
||||||
|
renderPagination(result);
|
||||||
|
} catch (error) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="5" class="text-center text-danger">Gagal memuat data.</td></tr>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPagination(data) {
|
||||||
|
const controls = document.getElementById('pagination-controls');
|
||||||
|
controls.innerHTML = '';
|
||||||
|
|
||||||
|
const prevBtn = document.createElement('button');
|
||||||
|
prevBtn.className = 'page-link-custom';
|
||||||
|
prevBtn.innerHTML = '« Previous';
|
||||||
|
prevBtn.disabled = data.current_page === 1;
|
||||||
|
prevBtn.onclick = () => fetchJurusan(data.current_page - 1);
|
||||||
|
controls.appendChild(prevBtn);
|
||||||
|
|
||||||
|
const pageBtn = document.createElement('button');
|
||||||
|
pageBtn.className = 'page-link-custom active';
|
||||||
|
pageBtn.innerText = data.current_page;
|
||||||
|
controls.appendChild(pageBtn);
|
||||||
|
|
||||||
|
const nextBtn = document.createElement('button');
|
||||||
|
nextBtn.className = 'page-link-custom';
|
||||||
|
nextBtn.innerHTML = 'Next »';
|
||||||
|
nextBtn.disabled = data.current_page === data.last_page;
|
||||||
|
nextBtn.onclick = () => fetchJurusan(data.current_page + 1);
|
||||||
|
controls.appendChild(nextBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteJurusan(id) {
|
||||||
|
const res = await Swal.fire({
|
||||||
|
title: 'Hapus data?',
|
||||||
|
text: "Data yang dihapus tidak dapat dikembalikan!",
|
||||||
|
icon: 'warning',
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonColor: '#ef4444',
|
||||||
|
confirmButtonText: 'Ya, Hapus'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.isConfirmed) {
|
||||||
|
const token = localStorage.getItem('access_token');
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/admin/jurusan/' + id, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { 'Authorization': 'Bearer ' + token }
|
||||||
|
});
|
||||||
|
if(response.ok) {
|
||||||
|
Swal.fire('Terhapus!', 'Data berhasil dihapus.', 'success');
|
||||||
|
fetchJurusan();
|
||||||
|
}
|
||||||
|
} catch (error) { Swal.fire('Error!', 'Koneksi gagal.', 'error'); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => fetchJurusan(1));
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
@extends('admin.layout')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<link rel="stylesheet" href="{{ asset('css/fitur-admin.css') }}">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
|
|
||||||
|
<div class="dashboard-section d-flex justify-content-center">
|
||||||
|
<div style="width: 100%; max-width: 700px;">
|
||||||
|
<div class="mb-4 text-center">
|
||||||
|
<h4 class="fw-bold text-dark">Tambah Jurusan Baru</h4>
|
||||||
|
<p class="text-muted small">Tambahkan pilihan program studi baru ke dalam sistem prediksi.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card border-0 shadow-sm rounded-4">
|
||||||
|
<div class="card-body p-5">
|
||||||
|
<form id="formCreateJurusan">
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-md-12 form-group-custom">
|
||||||
|
<label class="fw-bold small text-muted">Nama Jurusan</label>
|
||||||
|
<input type="text" id="nama_jurusan" class="form-control input-custom" placeholder="Contoh: Teknik Elektro" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-12 form-group-custom">
|
||||||
|
<label class="fw-bold small text-muted">Rumpun</label>
|
||||||
|
<select id="rumpun" class="form-select input-custom" required>
|
||||||
|
<option value="" disabled selected>Pilih Rumpun...</option>
|
||||||
|
<option value="Saintek">Saintek</option>
|
||||||
|
<option value="Soshum">Soshum</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-12 form-group-custom">
|
||||||
|
<label class="fw-bold small text-muted">Prospek Karir</label>
|
||||||
|
<textarea id="prospek_karir" class="form-control input-custom" rows="3" placeholder="Contoh: Engineer, Analyst, Developer"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-5 d-flex gap-2 justify-content-end">
|
||||||
|
<button type="submit" id="btnSave" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">Simpan Data</button>
|
||||||
|
<a href="{{ route('admin.jurusan') }}" class="btn btn-light px-4" style="border-radius: 10px;">Batal</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.getElementById('formCreateJurusan').addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const btn = document.getElementById('btnSave');
|
||||||
|
btn.disabled = true;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
nama_jurusan: document.getElementById('nama_jurusan').value,
|
||||||
|
rumpun: document.getElementById('rumpun').value,
|
||||||
|
prospek_karir: document.getElementById('prospek_karir').value
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/admin/jurusan', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': 'Bearer ' + localStorage.getItem('access_token'),
|
||||||
|
'Accept': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
Swal.fire({ icon: 'success', title: 'Berhasil!', confirmButtonColor: '#2D6A4F' })
|
||||||
|
.then(() => { window.location.href = "{{ route('admin.jurusan') }}"; });
|
||||||
|
}
|
||||||
|
} catch (error) { Swal.fire('Error!', 'Gagal menghubungi server.', 'error'); }
|
||||||
|
finally { btn.disabled = false; }
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
@extends('admin.layout')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<link rel="stylesheet" href="{{ asset('css/fitur-admin.css') }}">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
|
|
||||||
|
<div class="dashboard-section d-flex justify-content-center">
|
||||||
|
<div style="width: 100%; max-width: 700px;">
|
||||||
|
<div class="mb-4 text-center">
|
||||||
|
<h4 class="fw-bold text-dark">Edit Data Jurusan</h4>
|
||||||
|
<p class="text-muted small">Ubah informasi nama dan rumpun program studi.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card border-0 shadow-sm rounded-4">
|
||||||
|
<div class="card-body p-5">
|
||||||
|
<form id="formEditJurusan">
|
||||||
|
<input type="hidden" id="jurusan_id" value="{{ $id }}">
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-md-12 form-group-custom">
|
||||||
|
<label class="fw-bold small text-muted">Nama Jurusan</label>
|
||||||
|
<input type="text" id="nama_jurusan" class="form-control input-custom" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-12 form-group-custom">
|
||||||
|
<label class="fw-bold small text-muted">Rumpun</label>
|
||||||
|
<select id="rumpun" class="form-select input-custom" required>
|
||||||
|
<option value="Saintek">Saintek</option>
|
||||||
|
<option value="Soshum">Soshum</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-12 form-group-custom">
|
||||||
|
<label class="fw-bold small text-muted">Prospek Karir</label>
|
||||||
|
<textarea id="prospek_karir" class="form-control input-custom" rows="3"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-5 d-flex gap-2 justify-content-end">
|
||||||
|
<button type="submit" id="btnUpdate" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">Perbarui Data</button>
|
||||||
|
<a href="{{ route('admin.jurusan') }}" class="btn btn-light px-4" style="border-radius: 10px;">Batal</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const jurusanId = document.getElementById('jurusan_id').value;
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/admin/jurusan/${jurusanId}`, {
|
||||||
|
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('access_token') }
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
if (response.ok) {
|
||||||
|
document.getElementById('nama_jurusan').value = result.data.nama_jurusan;
|
||||||
|
document.getElementById('rumpun').value = result.data.rumpun;
|
||||||
|
document.getElementById('prospek_karir').value = result.data.prospek_karir || '';
|
||||||
|
}
|
||||||
|
} catch (error) { console.error(error); }
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('formEditJurusan').addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const btn = document.getElementById('btnUpdate');
|
||||||
|
btn.disabled = true;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
nama_jurusan: document.getElementById('nama_jurusan').value,
|
||||||
|
rumpun: document.getElementById('rumpun').value,
|
||||||
|
prospek_karir: document.getElementById('prospek_karir').value
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/admin/jurusan/${jurusanId}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': 'Bearer ' + localStorage.getItem('access_token'),
|
||||||
|
'Accept': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
Swal.fire({ icon: 'success', title: 'Berhasil Diperbarui!', confirmButtonColor: '#2D6A4F' })
|
||||||
|
.then(() => { window.location.href = "{{ route('admin.jurusan') }}"; });
|
||||||
|
}
|
||||||
|
} catch (error) { Swal.fire('Error!', 'Gagal koneksi.', 'error'); }
|
||||||
|
finally { btn.disabled = false; }
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', loadData);
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
@extends('admin.layout')
|
||||||
|
@section('content')
|
||||||
|
<div class="mb-4">
|
||||||
|
<h4 class="fw-bold">Pelatihan Model Random Forest</h4>
|
||||||
|
<p class="text-muted small">Kelola dataset alumni untuk meningkatkan akurasi sistem.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card border-0 shadow-sm rounded-4 p-4 text-center">
|
||||||
|
<div class="mb-3 text-success"><i class="fas fa-database fa-3x"></i></div>
|
||||||
|
<h6 class="fw-bold">Dataset Alumni</h6>
|
||||||
|
<h2 class="fw-black my-2">120</h2>
|
||||||
|
<p class="text-muted small">Data siap latih</p>
|
||||||
|
<button class="btn btn-success w-100 rounded-pill py-3 mt-3 fw-bold" onclick="runAI()">
|
||||||
|
JALANKAN TRAINING
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card border-0 shadow-sm rounded-4 p-4 bg-dark text-white font-monospace" style="min-height: 280px;">
|
||||||
|
<div id="ai-log">
|
||||||
|
<p class="text-success mb-1">> System Ready. Waiting for command...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function runAI() {
|
||||||
|
const log = document.getElementById('ai-log');
|
||||||
|
log.innerHTML += '<p class="mb-1 text-info">> Connecting to Flask API Python...</p>';
|
||||||
|
setTimeout(() => {
|
||||||
|
log.innerHTML += '<p class="mb-1">> Fetching dataset from MySQL...</p>';
|
||||||
|
log.innerHTML += '<p class="mb-1 text-warning">> Random Forest: Training 100 decision trees...</p>';
|
||||||
|
log.innerHTML += '<p class="mb-1 text-success">> Accuracy Result: 86.4%</p>';
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="id">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||||
|
|
||||||
|
<title>Admin - Sistem Rekomendasi Jurusan SMAN 4 Jember</title>
|
||||||
|
|
||||||
|
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600,700&display=swap" rel="stylesheet" />
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet"/>
|
||||||
|
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link href="{{ asset('css/admin.css') }}" rel="stylesheet">
|
||||||
|
|
||||||
|
@stack('styles')
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="admin-wrapper">
|
||||||
|
<aside class="sidebar-modern">
|
||||||
|
<div class="sidebar-header">
|
||||||
|
<img src="{{ asset('images/logo.png') }}" alt="Logo SMAN 4 Jember" onerror="this.src='https://ui-avatars.com/api/?name=S4&background=2D7A47&color=fff'">
|
||||||
|
<div class="sidebar-brand-text fw-bold text-white">SMAN 4 JEMBER</div>
|
||||||
|
<div class="sidebar-brand-sub small" style="opacity: 0.8;">SISTEM REKOMENDASI</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="sidebar-menu">
|
||||||
|
<div class="menu-label">Utama</div>
|
||||||
|
<a href="{{ route('admin.dashboard') }}" class="nav-item {{ request()->routeIs('admin.dashboard') ? 'active' : '' }}">
|
||||||
|
<i class="fas fa-th-large"></i> Dashboard
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="menu-label">Manajemen Data</div>
|
||||||
|
<a href="{{ route('admin.siswa') }}" class="nav-item {{ request()->routeIs('admin.siswa*') ? 'active' : '' }}">
|
||||||
|
<i class="fas fa-user-graduate"></i> Data Siswa
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('admin.jurusan') }}" class="nav-item {{ request()->routeIs('admin.jurusan*') ? 'active' : '' }}">
|
||||||
|
<i class="fas fa-university"></i> Data Jurusan
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="{{ route('admin.prospek-kerja') }}" class="nav-item {{ request()->routeIs('admin.prospek-kerja*') ? 'active' : '' }}">
|
||||||
|
<i class="fas fa-briefcase"></i> Data Prospek Kerja
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="{{ route('admin.guru') }}" class="nav-item {{ request()->routeIs('admin.guru*') ? 'active' : '' }}">
|
||||||
|
<i class="fas fa-user-tie"></i> Guru BK
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('admin.alumni') }}" class="nav-item {{ request()->routeIs('admin.alumni*') ? 'active' : '' }}">
|
||||||
|
<i class="fas fa-graduation-cap"></i> Data Alumni
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="menu-label">Analisis & Riwayat</div>
|
||||||
|
<a href="{{ route('admin.riwayat') }}" class="nav-item {{ request()->routeIs('admin.riwayat*') ? 'active' : '' }}">
|
||||||
|
<i class="fas fa-history"></i> Riwayat Prediksi
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('admin.latih-model') }}" class="nav-item {{ request()->routeIs('admin.latih-model') ? 'active' : '' }}">
|
||||||
|
<i class="fas fa-brain"></i> Latih Model (RF)
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="mt-4 pt-4 border-top border-white border-opacity-10">
|
||||||
|
<a href="{{ route('landingpage') }}" class="nav-item text-white opacity-75">
|
||||||
|
<i class="fas fa-sign-out-alt"></i> Keluar Ke Landing
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div class="main-content">
|
||||||
|
<header class="topbar">
|
||||||
|
<div class="search-bar">
|
||||||
|
<i class="fas fa-search text-muted"></i>
|
||||||
|
<input type="text" placeholder="Cari data...">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="user-profile d-flex align-items-center gap-3">
|
||||||
|
<div class="text-end d-none d-sm-block">
|
||||||
|
<div class="fw-bold small">Administrator</div>
|
||||||
|
<div class="text-muted" style="font-size: 11px;">GURU BK</div>
|
||||||
|
</div>
|
||||||
|
<img src="https://ui-avatars.com/api/?name=Admin+BK&background=2D7A47&color=fff&rounded=true" width="40" height="40">
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="p-4">
|
||||||
|
@yield('content')
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
@stack('scripts')
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
@extends('admin.layout')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<link rel="stylesheet" href="{{ asset('css/fitur-admin.css') }}">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
|
|
||||||
|
<div class="dashboard-section">
|
||||||
|
<div class="mb-4 d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<h4 class="fw-bold text-dark">Data Master Prospek Kerja</h4>
|
||||||
|
<p class="text-muted small">Kelola daftar karir dan estimasi gaji untuk setiap jurusan.</p>
|
||||||
|
</div>
|
||||||
|
<a href="{{ route('admin.prospek-kerja.create') }}" class="btn text-white px-4 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">
|
||||||
|
<i class="fas fa-plus me-2"></i>Tambah Prospek
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-container-modern shadow-sm">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-modern mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="text-center" width="60">No</th>
|
||||||
|
<th>Nama Profesi</th>
|
||||||
|
<th>Asal Jurusan</th>
|
||||||
|
<th>Estimasi Gaji</th>
|
||||||
|
<th class="text-center" width="150">Opsi</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="prospek-table-body">
|
||||||
|
<tr><td colspan="5" class="text-center py-5 text-muted">Memuat data...</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
async function fetchProspek() {
|
||||||
|
const token = localStorage.getItem('access_token');
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/admin/prospek-kerja`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer ' + token,
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
const tbody = document.getElementById('prospek-table-body');
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
|
||||||
|
if(!result.data || result.data.length === 0) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="5" class="text-center py-4">Tidak ada data prospek kerja.</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.data.forEach((item, index) => {
|
||||||
|
const namaJurusan = item.jurusan ? item.jurusan.nama_jurusan : '<span class="text-danger">Jurusan Dihapus</span>';
|
||||||
|
|
||||||
|
const row = `
|
||||||
|
<tr>
|
||||||
|
<td class="text-center text-muted">${index + 1}</td>
|
||||||
|
<td class="fw-bold text-dark">${item.nama_profesi}</td>
|
||||||
|
<td><span class="badge bg-light text-dark border px-3 py-2">${namaJurusan}</span></td>
|
||||||
|
<td class="small text-success fw-bold">${item.range_gaji || 'Belum diatur'}</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<div class="opsi-wrapper">
|
||||||
|
<a href="/admin/prospek-kerja/${item.id}/edit" class="btn-action btn-edit" title="Edit">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
<button onclick="deleteProspek(${item.id})" class="btn-action btn-delete" title="Hapus">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
tbody.innerHTML += row;
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
document.getElementById('prospek-table-body').innerHTML = '<tr><td colspan="5" class="text-center text-danger">Gagal memuat data.</td></tr>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteProspek(id) {
|
||||||
|
const res = await Swal.fire({
|
||||||
|
title: 'Hapus data?',
|
||||||
|
text: "Data yang dihapus tidak dapat dikembalikan!",
|
||||||
|
icon: 'warning',
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonColor: '#ef4444',
|
||||||
|
confirmButtonText: 'Ya, Hapus'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.isConfirmed) {
|
||||||
|
const token = localStorage.getItem('access_token');
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/admin/prospek-kerja/' + id, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { 'Authorization': 'Bearer ' + token }
|
||||||
|
});
|
||||||
|
if(response.ok) {
|
||||||
|
Swal.fire('Terhapus!', 'Data berhasil dihapus.', 'success');
|
||||||
|
fetchProspek();
|
||||||
|
}
|
||||||
|
} catch (error) { Swal.fire('Error!', 'Koneksi gagal.', 'error'); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', fetchProspek);
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
@extends('admin.layout')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<link rel="stylesheet" href="{{ asset('css/fitur-admin.css') }}">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
|
|
||||||
|
<div class="dashboard-section d-flex justify-content-center">
|
||||||
|
<div style="width: 100%; max-width: 700px;">
|
||||||
|
<div class="mb-4 text-center">
|
||||||
|
<h4 class="fw-bold text-dark">Tambah Prospek Kerja Baru</h4>
|
||||||
|
<p class="text-muted small">Tambahkan karir untuk direkomendasikan pada jurusan tertentu.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card border-0 shadow-sm rounded-4">
|
||||||
|
<div class="card-body p-5">
|
||||||
|
<form id="formCreateProspek">
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-md-12 form-group-custom">
|
||||||
|
<label class="fw-bold small text-muted">Kaitkan dengan Jurusan</label>
|
||||||
|
<select id="jurusan_id" class="form-select input-custom" required>
|
||||||
|
<option value="" disabled selected>Memuat daftar jurusan...</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-12 form-group-custom">
|
||||||
|
<label class="fw-bold small text-muted">Nama Profesi</label>
|
||||||
|
<input type="text" id="nama_profesi" class="form-control input-custom" placeholder="Contoh: Arsitek Profesional" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-12 form-group-custom">
|
||||||
|
<label class="fw-bold small text-muted">Range Gaji (Opsional)</label>
|
||||||
|
<input type="text" id="range_gaji" class="form-control input-custom" placeholder="Contoh: Rp 6.000.000 - Rp 15.000.000">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-12 form-group-custom">
|
||||||
|
<label class="fw-bold small text-muted">Deskripsi Pekerjaan (Opsional)</label>
|
||||||
|
<textarea id="deskripsi_pekerjaan" class="form-control input-custom" rows="3" placeholder="Jelaskan peran pekerjaan ini secara singkat..."></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-5 d-flex gap-2 justify-content-end">
|
||||||
|
<button type="submit" id="btnSave" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">Simpan Data</button>
|
||||||
|
<a href="{{ route('admin.prospek-kerja') }}" class="btn btn-light px-4" style="border-radius: 10px;">Batal</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
async function loadJurusan() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/admin/jurusan', {
|
||||||
|
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('access_token') }
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
const select = document.getElementById('jurusan_id');
|
||||||
|
select.innerHTML = '<option value="" disabled selected>Pilih Jurusan Terkait...</option>';
|
||||||
|
|
||||||
|
result.data.forEach(item => {
|
||||||
|
select.innerHTML += `<option value="${item.id}">${item.nama_jurusan}</option>`;
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
document.getElementById('jurusan_id').innerHTML = '<option disabled>Gagal memuat jurusan</option>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('formCreateProspek').addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const btn = document.getElementById('btnSave');
|
||||||
|
btn.disabled = true;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
jurusan_id: document.getElementById('jurusan_id').value,
|
||||||
|
nama_profesi: document.getElementById('nama_profesi').value,
|
||||||
|
range_gaji: document.getElementById('range_gaji').value,
|
||||||
|
deskripsi_pekerjaan: document.getElementById('deskripsi_pekerjaan').value
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/admin/prospek-kerja', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': 'Bearer ' + localStorage.getItem('access_token'),
|
||||||
|
'Accept': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
Swal.fire({ icon: 'success', title: 'Berhasil!', confirmButtonColor: '#2D6A4F' })
|
||||||
|
.then(() => { window.location.href = "{{ route('admin.prospek-kerja') }}"; });
|
||||||
|
} else {
|
||||||
|
Swal.fire('Error!', 'Periksa kembali isian Anda.', 'error');
|
||||||
|
}
|
||||||
|
} catch (error) { Swal.fire('Error!', 'Gagal menghubungi server.', 'error'); }
|
||||||
|
finally { btn.disabled = false; }
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', loadJurusan);
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
|
|
@ -0,0 +1,113 @@
|
||||||
|
@extends('admin.layout')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<link rel="stylesheet" href="{{ asset('css/fitur-admin.css') }}">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
|
|
||||||
|
<div class="dashboard-section d-flex justify-content-center">
|
||||||
|
<div style="width: 100%; max-width: 700px;">
|
||||||
|
<div class="mb-4 text-center">
|
||||||
|
<h4 class="fw-bold text-dark">Edit Data Prospek Kerja</h4>
|
||||||
|
<p class="text-muted small">Ubah informasi profesi atau estimasi gaji.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card border-0 shadow-sm rounded-4">
|
||||||
|
<div class="card-body p-5">
|
||||||
|
<form id="formEditProspek">
|
||||||
|
<input type="hidden" id="prospek_id" value="{{ $id }}">
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-md-12 form-group-custom">
|
||||||
|
<label class="fw-bold small text-muted">Kaitkan dengan Jurusan</label>
|
||||||
|
<select id="jurusan_id" class="form-select input-custom" required>
|
||||||
|
<option value="" disabled>Memuat daftar jurusan...</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-12 form-group-custom">
|
||||||
|
<label class="fw-bold small text-muted">Nama Profesi</label>
|
||||||
|
<input type="text" id="nama_profesi" class="form-control input-custom" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-12 form-group-custom">
|
||||||
|
<label class="fw-bold small text-muted">Range Gaji</label>
|
||||||
|
<input type="text" id="range_gaji" class="form-control input-custom">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-12 form-group-custom">
|
||||||
|
<label class="fw-bold small text-muted">Deskripsi Pekerjaan</label>
|
||||||
|
<textarea id="deskripsi_pekerjaan" class="form-control input-custom" rows="3"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-5 d-flex gap-2 justify-content-end">
|
||||||
|
<button type="submit" id="btnUpdate" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">Perbarui Data</button>
|
||||||
|
<a href="{{ route('admin.prospek-kerja') }}" class="btn btn-light px-4" style="border-radius: 10px;">Batal</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const prospekId = document.getElementById('prospek_id').value;
|
||||||
|
|
||||||
|
async function loadJurusanAndData() {
|
||||||
|
const token = localStorage.getItem('access_token');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resJurusan = await fetch('/api/admin/jurusan', {
|
||||||
|
headers: { 'Authorization': 'Bearer ' + token }
|
||||||
|
});
|
||||||
|
const resultJurusan = await resJurusan.json();
|
||||||
|
const select = document.getElementById('jurusan_id');
|
||||||
|
select.innerHTML = '<option value="" disabled>Pilih Jurusan Terkait...</option>';
|
||||||
|
|
||||||
|
resultJurusan.data.forEach(item => {
|
||||||
|
select.innerHTML += `<option value="${item.id}">${item.nama_jurusan}</option>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const resProspek = await fetch(`/api/admin/prospek-kerja/${prospekId}`, {
|
||||||
|
headers: { 'Authorization': 'Bearer ' + token }
|
||||||
|
});
|
||||||
|
const resultProspek = await resProspek.json();
|
||||||
|
|
||||||
|
if (resProspek.ok) {
|
||||||
|
document.getElementById('jurusan_id').value = resultProspek.data.jurusan_id;
|
||||||
|
document.getElementById('nama_profesi').value = resultProspek.data.nama_profesi;
|
||||||
|
document.getElementById('range_gaji').value = resultProspek.data.range_gaji || '';
|
||||||
|
document.getElementById('deskripsi_pekerjaan').value = resultProspek.data.deskripsi_pekerjaan || '';
|
||||||
|
}
|
||||||
|
} catch (error) { console.error(error); }
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('formEditProspek').addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const btn = document.getElementById('btnUpdate');
|
||||||
|
btn.disabled = true;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
jurusan_id: document.getElementById('jurusan_id').value,
|
||||||
|
nama_profesi: document.getElementById('nama_profesi').value,
|
||||||
|
range_gaji: document.getElementById('range_gaji').value,
|
||||||
|
deskripsi_pekerjaan: document.getElementById('deskripsi_pekerjaan').value
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/admin/prospek-kerja/${prospekId}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': 'Bearer ' + localStorage.getItem('access_token'),
|
||||||
|
'Accept': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
Swal.fire({ icon: 'success', title: 'Berhasil Diperbarui!', confirmButtonColor: '#2D6A4F' })
|
||||||
|
.then(() => { window.location.href = "{{ route('admin.prospek-kerja') }}"; });
|
||||||
|
}
|
||||||
|
} catch (error) { Swal.fire('Error!', 'Gagal koneksi.', 'error'); }
|
||||||
|
finally { btn.disabled = false; }
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', loadJurusanAndData);
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
|
|
@ -0,0 +1,166 @@
|
||||||
|
@extends('admin.layout')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="mb-4">
|
||||||
|
<h4 class="fw-bold text-dark">Riwayat Prediksi Jurusan</h4>
|
||||||
|
<p class="text-muted small">Hasil prediksi algoritma Random Forest berdasarkan profil siswa.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card border-0 shadow-sm rounded-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover align-middle">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th class="py-3 px-4">Tanggal</th>
|
||||||
|
<th class="py-3">Nama Siswa</th>
|
||||||
|
<th class="py-3">Hasil Rekomendasi</th>
|
||||||
|
<th class="py-3 text-center">Aksi</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="table-riwayat">
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="text-center py-4 text-muted">
|
||||||
|
<i class="fas fa-spinner fa-spin me-2"></i> Memuat data riwayat...
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal fade" id="detailModal" tabindex="-1" aria-labelledby="detailModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content border-0 shadow">
|
||||||
|
<div class="modal-header border-bottom-0 pb-0">
|
||||||
|
<h5 class="modal-title fw-bold" id="detailModalLabel">Detail Prediksi Jurusan</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body p-4" id="modal-body-content">
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer border-top-0 pt-0">
|
||||||
|
<button type="button" class="btn btn-secondary px-4 rounded-pill shadow-none" data-bs-dismiss="modal">Tutup</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@push('scripts')
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', async function() {
|
||||||
|
const token = localStorage.getItem('access_token');
|
||||||
|
|
||||||
|
// Cek sesi login
|
||||||
|
if (!token) {
|
||||||
|
window.location.href = '/login';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tbody = document.getElementById('table-riwayat');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Memanggil API riwayat
|
||||||
|
const response = await fetch('/api/admin/riwayat', {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
const data = result.data;
|
||||||
|
|
||||||
|
// Jika data kosong
|
||||||
|
if (data.length === 0) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="4" class="text-center py-4 text-muted">Belum ada riwayat prediksi yang dilakukan siswa.</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render data riwayat ke dalam tabel
|
||||||
|
tbody.innerHTML = data.map(item => `
|
||||||
|
<tr>
|
||||||
|
<td class="px-4 text-muted">${item.tanggal}</td>
|
||||||
|
<td class="fw-bold">${item.nama_siswa}</td>
|
||||||
|
<td><span class="badge bg-success px-3 py-2 rounded-pill shadow-none">${item.hasil}</span></td>
|
||||||
|
<td class="text-center">
|
||||||
|
<button class="btn btn-sm btn-light px-3 rounded-pill text-primary fw-medium" onclick="showDetail(${item.id})">
|
||||||
|
<i class="fas fa-file-alt me-1"></i> Detail
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
} else {
|
||||||
|
tbody.innerHTML = `<tr><td colspan="4" class="text-center py-4 text-danger">Gagal memuat data: ${result.message}</td></tr>`;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching riwayat:', error);
|
||||||
|
tbody.innerHTML = '<tr><td colspan="4" class="text-center py-4 text-danger">Terjadi kesalahan jaringan atau server.</td></tr>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mengambil dan menampilkan detail riwayat di dalam Modal
|
||||||
|
*/
|
||||||
|
async function showDetail(id) {
|
||||||
|
const token = localStorage.getItem('access_token');
|
||||||
|
const modalBody = document.getElementById('modal-body-content');
|
||||||
|
|
||||||
|
// Tampilkan modal dengan state loading
|
||||||
|
const detailModal = new bootstrap.Modal(document.getElementById('detailModal'));
|
||||||
|
detailModal.show();
|
||||||
|
|
||||||
|
modalBody.innerHTML = `
|
||||||
|
<div class="text-center py-4">
|
||||||
|
<i class="fas fa-spinner fa-spin text-primary fs-3 mb-2"></i>
|
||||||
|
<p class="text-muted m-0">Mengambil detail...</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Memanggil API detail riwayat
|
||||||
|
const response = await fetch(`/api/admin/riwayat/${id}`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
const data = result.data;
|
||||||
|
|
||||||
|
// Render detail ke dalam modal
|
||||||
|
modalBody.innerHTML = `
|
||||||
|
<div class="mb-3 border-bottom pb-2">
|
||||||
|
<span class="text-muted d-block small mb-1">Nama Lengkap Siswa</span>
|
||||||
|
<span class="fw-bold text-dark fs-5">${data.nama_siswa}</span>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3 border-bottom pb-2">
|
||||||
|
<span class="text-muted d-block small mb-1">Email Siswa</span>
|
||||||
|
<span class="text-dark">${data.email_siswa}</span>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3 border-bottom pb-2">
|
||||||
|
<span class="text-muted d-block small mb-1">Waktu Prediksi Dibuat</span>
|
||||||
|
<span class="text-dark">${data.tanggal}</span>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<span class="text-muted d-block small mb-2">Hasil Rekomendasi Random Forest</span>
|
||||||
|
<span class="badge bg-success px-3 py-2 rounded-pill fs-6 shadow-none">${data.hasil_rekomendasi}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
modalBody.innerHTML = `<div class="alert alert-danger m-0">Gagal memuat detail: ${result.message}</div>`;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching detail:', error);
|
||||||
|
modalBody.innerHTML = `<div class="alert alert-danger m-0">Terjadi kesalahan jaringan saat mengambil detail.</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
|
@endsection
|
||||||
|
|
@ -0,0 +1,152 @@
|
||||||
|
@extends('admin.layout')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<link rel="stylesheet" href="{{ asset('css/fitur-admin.css') }}">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
|
|
||||||
|
<div class="dashboard-section">
|
||||||
|
<div class="mb-4 d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<h4 class="fw-bold text-dark">Manajemen Data Siswa</h4>
|
||||||
|
<p class="text-muted small">Daftar siswa SMAN 4 Jember yang terdaftar di sistem.</p>
|
||||||
|
</div>
|
||||||
|
<a href="{{ route('admin.siswa.create') }}" class="btn text-white px-4 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">
|
||||||
|
<i class="fas fa-plus me-2"></i>Tambah Siswa
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-container-modern shadow-sm">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-modern mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="text-center" width="80">ID</th>
|
||||||
|
<th>Nama Lengkap</th>
|
||||||
|
<th>NISN</th>
|
||||||
|
<th class="text-center">Kelas</th>
|
||||||
|
<th class="text-center" width="150">Opsi</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="siswa-table-body">
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pagination-container">
|
||||||
|
<div class="text-muted small" id="pagination-info">
|
||||||
|
Menampilkan 0 sampai 0 dari 0 data
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-1" id="pagination-controls">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
async function fetchSiswa(page = 1) {
|
||||||
|
const token = localStorage.getItem('access_token');
|
||||||
|
const tbody = document.getElementById('siswa-table-body');
|
||||||
|
|
||||||
|
tbody.innerHTML = '<tr><td colspan="5" class="text-center py-5">Memuat data...</td></tr>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/admin/siswa?page=${page}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer ' + token,
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
|
||||||
|
if (!result.data || result.data.length === 0) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="5" class="text-center py-5">Belum ada data siswa.</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.data.forEach((item) => {
|
||||||
|
const row = `
|
||||||
|
<tr>
|
||||||
|
<td class="text-center fw-bold text-muted">${item.id_siswa}</td>
|
||||||
|
<td class="fw-bold text-dark">${item.nama_lengkap}</td>
|
||||||
|
<td class="text-muted">${item.nisn}</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<span class="badge rounded-pill badge-kelas">${item.kelas}</span>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<div class="opsi-wrapper">
|
||||||
|
<a href="/admin/siswa/${item.id}/edit" class="btn-action btn-edit" title="Edit">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
<button onclick="deleteSiswa(${item.id})" class="btn-action btn-delete" title="Hapus">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
tbody.innerHTML += row;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('pagination-info').innerText =
|
||||||
|
`Menampilkan ${result.from || 0} sampai ${result.to || 0} dari ${result.total || 0} data`;
|
||||||
|
|
||||||
|
renderPagination(result);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="5" class="text-center py-5 text-danger">Gagal memuat data.</td></tr>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPagination(data) {
|
||||||
|
const controls = document.getElementById('pagination-controls');
|
||||||
|
controls.innerHTML = '';
|
||||||
|
|
||||||
|
const prevBtn = document.createElement('button');
|
||||||
|
prevBtn.className = 'page-link-custom';
|
||||||
|
prevBtn.innerHTML = '« Previous';
|
||||||
|
prevBtn.disabled = data.current_page === 1;
|
||||||
|
prevBtn.onclick = () => fetchSiswa(data.current_page - 1);
|
||||||
|
controls.appendChild(prevBtn);
|
||||||
|
|
||||||
|
const pageBtn = document.createElement('button');
|
||||||
|
pageBtn.className = 'page-link-custom active';
|
||||||
|
pageBtn.innerText = data.current_page || 1;
|
||||||
|
controls.appendChild(pageBtn);
|
||||||
|
|
||||||
|
const nextBtn = document.createElement('button');
|
||||||
|
nextBtn.className = 'page-link-custom';
|
||||||
|
nextBtn.innerHTML = 'Next »';
|
||||||
|
nextBtn.disabled = data.current_page === data.last_page;
|
||||||
|
nextBtn.onclick = () => fetchSiswa(data.current_page + 1);
|
||||||
|
controls.appendChild(nextBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteSiswa(id) {
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Hapus data?',
|
||||||
|
text: "Data akan dihapus permanen!",
|
||||||
|
icon: 'warning',
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonColor: '#ef4444',
|
||||||
|
confirmButtonText: 'Ya, Hapus'
|
||||||
|
}).then(async (result) => {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
const token = localStorage.getItem('access_token');
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/admin/siswa/' + id, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { 'Authorization': 'Bearer ' + token, 'Accept': 'application/json' }
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
Swal.fire('Berhasil!', 'Data telah dihapus.', 'success');
|
||||||
|
fetchSiswa();
|
||||||
|
}
|
||||||
|
} catch (error) { Swal.fire('Error!', 'Koneksi gagal.', 'error'); }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => fetchSiswa(1));
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
|
|
@ -0,0 +1,104 @@
|
||||||
|
@extends('admin.layout')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<link rel="stylesheet" href="{{ asset('css/fitur-admin.css') }}">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
|
|
||||||
|
<div class="dashboard-section d-flex justify-content-center">
|
||||||
|
<div style="width: 100%; max-width: 700px;">
|
||||||
|
<div class="mb-4 text-center">
|
||||||
|
<h4 class="fw-bold text-dark">Tambah Siswa Baru</h4>
|
||||||
|
<p class="text-muted small text-uppercase fw-bold" style="letter-spacing: 1px;">Sistem Rekomendasi SMAN 4 Jember</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card border-0 shadow-sm rounded-4">
|
||||||
|
<div class="card-body p-5">
|
||||||
|
<form id="createSiswaForm">
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-md-12 form-group-custom">
|
||||||
|
<label>Nama Lengkap</label>
|
||||||
|
<input type="text" id="nama_lengkap" class="form-control input-custom" required placeholder="Masukkan nama lengkap siswa">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-12 form-group-custom">
|
||||||
|
<label>NISN</label>
|
||||||
|
<input type="text" id="nisn" class="form-control input-custom" required placeholder="Masukkan 10 digit NISN">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-12 form-group-custom">
|
||||||
|
<label>Kelas</label>
|
||||||
|
<input type="text" id="kelas" class="form-control input-custom" required placeholder="Contoh: XII IPA 1">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-12 form-group-custom">
|
||||||
|
<label>Peminatan</label>
|
||||||
|
<select id="peminatan" class="form-select input-custom">
|
||||||
|
<option value="" disabled selected>Pilih Peminatan</option>
|
||||||
|
<option value="Saintek">Saintek</option>
|
||||||
|
<option value="Soshum">Soshum</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-12 form-group-custom">
|
||||||
|
<label>User ID (Sesuai Akun Login)</label>
|
||||||
|
<input type="number" id="user_id" class="form-control input-custom" required placeholder="Contoh: 1">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-12 form-group-custom">
|
||||||
|
<label>Jurusan ID</label>
|
||||||
|
<input type="number" id="jurusan_id" class="form-control input-custom" required placeholder="Contoh: 1">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-5 d-flex gap-2 justify-content-end">
|
||||||
|
<button type="submit" id="btnSave" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">Simpan Data</button>
|
||||||
|
<a href="{{ route('admin.siswa') }}" class="btn btn-light px-4" style="border-radius: 10px;">Batal</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.getElementById('createSiswaForm').addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const btn = document.getElementById('btnSave');
|
||||||
|
btn.disabled = true;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
user_id: document.getElementById('user_id').value,
|
||||||
|
jurusan_id: document.getElementById('jurusan_id').value,
|
||||||
|
nama_lengkap: document.getElementById('nama_lengkap').value,
|
||||||
|
nisn: document.getElementById('nisn').value,
|
||||||
|
kelas: document.getElementById('kelas').value,
|
||||||
|
peminatan: document.getElementById('peminatan').value,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/admin/siswa', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': 'Bearer ' + localStorage.getItem('access_token'),
|
||||||
|
'Accept': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
Swal.fire({ icon: 'success', title: 'Berhasil!', text: 'Data siswa ditambahkan.', confirmButtonColor: '#2D7A47' })
|
||||||
|
.then(() => { window.location.href = "{{ route('admin.siswa') }}"; });
|
||||||
|
} else {
|
||||||
|
Swal.fire('Gagal!', result.message || 'Cek kembali data Anda.', 'warning');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Swal.fire('Error!', 'Gagal menghubungi server.', 'error');
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
|
|
@ -0,0 +1,122 @@
|
||||||
|
@extends('admin.layout')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<link rel="stylesheet" href="{{ asset('css/fitur-admin.css') }}">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
|
|
||||||
|
<div class="dashboard-section d-flex justify-content-center">
|
||||||
|
<div style="width: 100%; max-width: 700px;">
|
||||||
|
<div class="mb-4 text-center">
|
||||||
|
<h4 class="fw-bold text-dark">Edit Data Siswa</h4>
|
||||||
|
<p id="display_id_siswa" class="text-muted small fw-bold">Memuat data...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card border-0 shadow-sm rounded-4">
|
||||||
|
<div class="card-body p-5">
|
||||||
|
<form id="editSiswaForm">
|
||||||
|
<input type="hidden" id="siswa_id_db" value="{{ $id }}">
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-md-12 form-group-custom">
|
||||||
|
<label>Nama Lengkap</label>
|
||||||
|
<input type="text" id="nama_lengkap" class="form-control input-custom" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-12 form-group-custom">
|
||||||
|
<label>NISN</label>
|
||||||
|
<input type="text" id="nisn" class="form-control input-custom" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-12 form-group-custom">
|
||||||
|
<label>Kelas</label>
|
||||||
|
<input type="text" id="kelas" class="form-control input-custom" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-12 form-group-custom">
|
||||||
|
<label>Peminatan</label>
|
||||||
|
<select id="peminatan" class="form-select input-custom">
|
||||||
|
<option value="Saintek">Saintek</option>
|
||||||
|
<option value="Soshum">Soshum</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-5 d-flex gap-2 justify-content-end">
|
||||||
|
<button type="submit" id="btnUpdate" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">Perbarui Data</button>
|
||||||
|
<a href="{{ route('admin.siswa') }}" class="btn btn-light px-4" style="border-radius: 10px;">Batal</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const dbId = document.getElementById('siswa_id_db').value;
|
||||||
|
|
||||||
|
// Mengambil data lama untuk ditampilkan di form
|
||||||
|
async function loadSiswa() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/admin/siswa/${dbId}`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer ' + localStorage.getItem('access_token'),
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
if (response.ok) {
|
||||||
|
document.getElementById('nama_lengkap').value = result.data.nama_lengkap;
|
||||||
|
document.getElementById('nisn').value = result.data.nisn;
|
||||||
|
document.getElementById('kelas').value = result.data.kelas;
|
||||||
|
document.getElementById('peminatan').value = result.data.peminatan;
|
||||||
|
document.getElementById('display_id_siswa').innerText = `EDITING: ${result.data.id_siswa}`;
|
||||||
|
} else {
|
||||||
|
Swal.fire('Gagal!', 'Data tidak ditemukan.', 'warning');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Swal.fire('Error!', 'Gagal mengambil data dari server.', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mengirim data perbaikan ke API
|
||||||
|
document.getElementById('editSiswaForm').addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const btn = document.getElementById('btnUpdate');
|
||||||
|
btn.disabled = true;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
nama_lengkap: document.getElementById('nama_lengkap').value,
|
||||||
|
nisn: document.getElementById('nisn').value,
|
||||||
|
kelas: document.getElementById('kelas').value,
|
||||||
|
peminatan: document.getElementById('peminatan').value
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/admin/siswa/${dbId}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': 'Bearer ' + localStorage.getItem('access_token'),
|
||||||
|
'Accept': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
Swal.fire({ icon: 'success', title: 'Berhasil Diperbarui!', text: 'Data siswa telah diperbarui.', confirmButtonColor: '#2D7A47' })
|
||||||
|
.then(() => { window.location.href = "{{ route('admin.siswa') }}"; });
|
||||||
|
} else {
|
||||||
|
const result = await response.json();
|
||||||
|
Swal.fire('Gagal!', result.message || 'Cek inputan.', 'warning');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Swal.fire('Error!', 'Kesalahan koneksi server.', 'error');
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Jalankan load data saat halaman siap
|
||||||
|
document.addEventListener('DOMContentLoaded', loadSiswa);
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
|
|
@ -0,0 +1,129 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="id">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Login ASGARD - SMAN 4 Jember</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<link href="{{ asset('css/auth.css') }}" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body class="auth-page px-6">
|
||||||
|
|
||||||
|
<div class="login-container">
|
||||||
|
<div class="login-visual">
|
||||||
|
<div class="visual-pattern"></div>
|
||||||
|
<div class="relative z-10">
|
||||||
|
<div class="mb-12">
|
||||||
|
<span class="badge-ai">AI TECHNOLOGY</span>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-5xl font-extrabold italic tracking-tighter mb-6 leading-tight">
|
||||||
|
Tentukan Jalur <br> Karirmu Bersama <br> <span class="text-green-400">ASGARD.</span>
|
||||||
|
</h2>
|
||||||
|
<p class="text-green-100/60 font-medium leading-relaxed max-w-sm mb-12">
|
||||||
|
Sistem bimbingan cerdas SMAN 4 Jember membantu memetakan masa depanmu melalui kekuatan data.
|
||||||
|
</p>
|
||||||
|
<div class="flex items-center space-x-4 opacity-50">
|
||||||
|
<div class="w-10 h-10 rounded-full border border-white flex items-center justify-center font-bold">S4</div>
|
||||||
|
<div class="text-[10px] font-bold uppercase tracking-widest">Official Academic System <br> SMAN 4 Jember</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="login-form-area">
|
||||||
|
<div class="mb-10">
|
||||||
|
<h3 class="text-3xl font-black text-slate-900 tracking-tighter">AI Login <span class="text-green-600">—</span></h3>
|
||||||
|
<p class="text-slate-400 font-bold text-[10px] uppercase tracking-widest mt-2">Sistem Rekomendasi Jurusan</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="loginForm" class="space-y-6">
|
||||||
|
@csrf
|
||||||
|
<div>
|
||||||
|
<label class="block text-[10px] font-black uppercase tracking-widest text-slate-400 mb-3 ml-1">Username / ID Siswa</label>
|
||||||
|
<input type="text" id="username" name="username" required
|
||||||
|
class="input-modern w-full text-slate-900"
|
||||||
|
placeholder="Masukkan ID Siswa">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-between items-center mb-3 px-1">
|
||||||
|
<label class="block text-[10px] font-black uppercase tracking-widest text-slate-400">Password</label>
|
||||||
|
<a href="#" class="text-[10px] font-black text-green-600 hover:text-green-800 uppercase tracking-widest">Bantuan</a>
|
||||||
|
</div>
|
||||||
|
<input type="password" id="password" name="password" required
|
||||||
|
class="input-modern w-full text-slate-900"
|
||||||
|
placeholder="••••••••">
|
||||||
|
<p id="error-message" class="hidden text-red-500 text-[11px] font-bold mt-2 ml-1 italic"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pt-4 flex flex-col gap-4">
|
||||||
|
<button type="submit" id="btnLogin" class="btn-login w-full">
|
||||||
|
Masuk Ke ASGARD
|
||||||
|
</button>
|
||||||
|
<a href="{{ route('landingpage') }}" class="btn-login w-full text-center flex items-center justify-center">
|
||||||
|
Kembali ke Beranda
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="mt-12 text-center">
|
||||||
|
<p class="text-[11px] text-slate-400 font-bold uppercase tracking-widest">
|
||||||
|
Punya kendala akses?
|
||||||
|
<a href="#" class="text-green-600 hover:text-green-800 ml-1 underline">Hubungi Guru BK</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.getElementById('loginForm').addEventListener('submit', async function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const btn = document.getElementById('btnLogin');
|
||||||
|
const username = document.getElementById('username').value;
|
||||||
|
const password = document.getElementById('password').value;
|
||||||
|
const errorElement = document.getElementById('error-message');
|
||||||
|
|
||||||
|
errorElement.classList.add('hidden');
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.innerText = 'Memproses...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
username: username,
|
||||||
|
password: password
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
localStorage.setItem('access_token', data.access_token);
|
||||||
|
localStorage.setItem('user_role', data.role);
|
||||||
|
|
||||||
|
if (data.role === 'admin') {
|
||||||
|
window.location.href = "{{ route('admin.dashboard') }}";
|
||||||
|
} else {
|
||||||
|
window.location.href = "{{ route('student.profile') }}";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
errorElement.innerText = data.message || 'Username atau password salah.';
|
||||||
|
errorElement.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
errorElement.innerText = 'Koneksi gagal. Silakan coba lagi.';
|
||||||
|
errorElement.classList.remove('hidden');
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerText = 'Masuk Ke ASGARD';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,179 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="id">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>ASGARD - Rekomendasi Jurusan SMAN 4 Jember</title>
|
||||||
|
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<link href="{{ asset('css/landingpage.css') }}" rel="stylesheet">
|
||||||
|
<link href="https://unpkg.com/aos@2.3.1/dist/aos.css" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css" />
|
||||||
|
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;700;800&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
body { font-family: 'Plus Jakarta Sans', sans-serif; }
|
||||||
|
.text-gradient {
|
||||||
|
background: linear-gradient(to right, #10b981, #059669);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body x-data="{ atTop: true }" @scroll.window="atTop = (window.pageYOffset > 50 ? false : true)" class="flex flex-col min-h-screen">
|
||||||
|
|
||||||
|
<nav class="fixed w-full z-50 transition-all duration-500 px-6 py-4" :class="atTop ? 'bg-transparent' : 'bg-white/90 backdrop-blur-xl shadow-lg border-b border-green-100'">
|
||||||
|
<div class="max-w-7xl mx-auto flex items-center justify-between">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<span class="font-black text-2xl tracking-tighter text-emerald-900 italic">ASGARD<span class="text-green-500">.</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="hidden md:flex space-x-10 text-[11px] font-bold uppercase tracking-widest text-emerald-900">
|
||||||
|
<a href="#tentang" class="hover:text-green-600 transition">Tentang</a>
|
||||||
|
<a href="#fitur" class="hover:text-green-600 transition">Variabel</a>
|
||||||
|
<a href="#faq" class="hover:text-green-600 transition">FAQ</a>
|
||||||
|
</div>
|
||||||
|
<a href="{{ route('login') }}" class="bg-emerald-900 text-white px-8 py-3 rounded-full font-bold hover:bg-black hover:scale-105 transition-all">LOGIN</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main class="flex-grow">
|
||||||
|
<header class="pt-32 pb-20 overflow-hidden">
|
||||||
|
<div class="max-w-7xl mx-auto px-6">
|
||||||
|
<div class="swiper heroSwiper" data-aos="fade-up">
|
||||||
|
<div class="swiper-wrapper">
|
||||||
|
<div class="swiper-slide text-center py-12">
|
||||||
|
<h1 class="text-6xl md:text-8xl font-black text-emerald-950 mb-6 leading-none tracking-tighter">
|
||||||
|
Wujudkan Impianmu <br> Bersama <span class="text-gradient">ASGARD.</span>
|
||||||
|
</h1>
|
||||||
|
<p class="max-w-2xl mx-auto text-emerald-900/60 text-lg mb-10 font-medium">
|
||||||
|
Sistem cerdas penentu masa depan siswa SMAN 4 Jember.
|
||||||
|
</p>
|
||||||
|
<a href="{{ route('login') }}" class="bg-green-600 text-white px-12 py-5 rounded-2xl font-bold uppercase text-xs tracking-widest shadow-2xl active:scale-95 transition-all">Mulai Temukan Jurusan</a>
|
||||||
|
</div>
|
||||||
|
<div class="swiper-slide text-center py-12">
|
||||||
|
<h2 class="text-5xl font-black text-emerald-950 mb-6">Kenapa ASGARD?</h2>
|
||||||
|
<p class="max-w-xl mx-auto text-emerald-900/60 mb-8 text-lg italic leading-relaxed">
|
||||||
|
"Membantu 87% mahasiswa yang sering merasa salah jurusan dengan bimbingan berbasis data objektif."
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="swiper-pagination"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section id="tentang" class="py-24 px-6">
|
||||||
|
<div class="max-w-7xl mx-auto grid md:grid-cols-2 gap-16 items-center">
|
||||||
|
<div data-aos="fade-right">
|
||||||
|
<h2 class="text-4xl font-black text-emerald-950 mb-8 uppercase italic tracking-tighter">Apa itu ASGARD?</h2>
|
||||||
|
<p class="text-slate-600 leading-relaxed text-lg mb-6">
|
||||||
|
<strong>ASGARD</strong> adalah singkatan dari <em>AI System for Graduation & Academic Recommendation</em>. Ini adalah platform rekomendasi jurusan kuliah yang dirancang khusus untuk memandu siswa SMAN 4 Jember menemukan jalur karir terbaik.
|
||||||
|
</p>
|
||||||
|
<p class="text-slate-600 leading-relaxed text-lg font-medium border-l-4 border-green-500 pl-6">
|
||||||
|
Sistem ini tidak hanya menebak, tapi menghitung potensi diri berdasarkan variabel nyata yang ada pada profilmu.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center" data-aos="zoom-in">
|
||||||
|
<img src="https://illustrations.popsy.co/emerald/student-going-to-school.svg" alt="Student" class="w-full max-w-sm animate-float">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="fitur" class="py-24 px-6 bg-green-50/50">
|
||||||
|
<div class="max-w-7xl mx-auto">
|
||||||
|
<div class="text-center mb-16">
|
||||||
|
<h2 class="text-4xl font-black text-emerald-950 italic uppercase">Variabel Analisis Kami</h2>
|
||||||
|
<p class="text-emerald-900/50 font-bold tracking-widest text-[10px] mt-2 uppercase">Dasar Prediksi Rekomendasi</p>
|
||||||
|
</div>
|
||||||
|
<div class="grid md:grid-cols-4 gap-6">
|
||||||
|
<div class="bg-white rounded-3xl p-10 flex flex-col justify-between h-80 shadow-sm border border-emerald-100" data-aos="fade-up">
|
||||||
|
<span class="text-4xl">📝</span>
|
||||||
|
<div>
|
||||||
|
<h4 class="font-black text-emerald-950 mb-2 uppercase">Nilai Rapor</h4>
|
||||||
|
<p class="text-sm text-slate-500 leading-relaxed">Analisis performa akademikmu selama 5 semester terakhir.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-emerald-900 rounded-3xl p-10 flex flex-col justify-between h-80 text-white shadow-xl" data-aos="fade-up" data-aos-delay="100">
|
||||||
|
<span class="text-4xl">🧠</span>
|
||||||
|
<div>
|
||||||
|
<h4 class="font-black mb-2 uppercase text-green-400">Minat & Bakat</h4>
|
||||||
|
<p class="text-sm text-emerald-100/60 leading-relaxed">Memetakan kecenderungan bakatmu melalui kuesioner psikologi.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-yellow-400 rounded-3xl p-10 flex flex-col justify-between h-80 shadow-sm" data-aos="fade-up" data-aos-delay="200">
|
||||||
|
<span class="text-4xl">💰</span>
|
||||||
|
<div>
|
||||||
|
<h4 class="font-black text-emerald-900 mb-2 uppercase">Kondisi Ekonomi</h4>
|
||||||
|
<p class="text-sm text-emerald-900/60 leading-relaxed font-bold">Menyesuaikan pilihan jurusan dengan kesiapan finansial keluarga.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white rounded-3xl p-10 flex flex-col justify-between h-80 shadow-sm border border-emerald-100" data-aos="fade-up" data-aos-delay="300">
|
||||||
|
<span class="text-4xl">🎓</span>
|
||||||
|
<div>
|
||||||
|
<h4 class="font-black text-emerald-950 mb-2 uppercase">Data Alumni</h4>
|
||||||
|
<p class="text-sm text-slate-500 leading-relaxed">Dicocokkan dengan tingkat keberhasilan alumni SMAPA di berbagai PTN.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="faq" class="py-24 px-6">
|
||||||
|
<div class="max-w-3xl mx-auto" x-data="{ active: 1 }">
|
||||||
|
<h2 class="text-center text-4xl font-black text-emerald-950 mb-16 italic">Pertanyaan Umum</h2>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="bg-white rounded-3xl border border-green-100 overflow-hidden shadow-sm">
|
||||||
|
<button @click="active = (active === 1 ? null : 1)" class="w-full px-8 py-6 text-left font-bold text-emerald-900 flex justify-between items-center">
|
||||||
|
<span>Bagaimana cara kerja ASGARD?</span>
|
||||||
|
<svg class="w-5 h-5 transition-transform" :class="active === 1 ? 'rotate-180' : ''" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M19 9l-7 7-7-7"></path></svg>
|
||||||
|
</button>
|
||||||
|
<div x-show="active === 1" class="px-8 pb-6 text-slate-500 leading-relaxed">ASGARD memproses semua variabel profilmu menggunakan algoritma AI untuk menghasilkan persentase kecocokan jurusan yang paling optimal.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="bg-[#062C22] text-white py-16 px-6">
|
||||||
|
<div class="max-w-7xl mx-auto grid grid-cols-1 md:grid-cols-3 gap-12">
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<span class="font-black text-2xl tracking-tighter uppercase italic">ASGARD.</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-slate-300 text-sm leading-relaxed max-w-xs">
|
||||||
|
Sistem pakar rekomendasi jurusan kuliah SMAN 4 Jember berbasis data-driven.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h5 class="font-bold text-xs uppercase tracking-widest text-emerald-400 mb-6 italic">Informasi</h5>
|
||||||
|
<ul class="space-y-4 text-sm text-slate-300">
|
||||||
|
<li><a href="{{ route('panduan') }}" class="hover:text-white transition">Panduan Sistem</a></li>
|
||||||
|
<li><a href="#" class="hover:text-white transition">Kebijakan Privasi</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h5 class="font-bold text-xs uppercase tracking-widest text-emerald-400 mb-6 italic">Sekolah</h5>
|
||||||
|
<ul class="space-y-4 text-sm text-slate-300">
|
||||||
|
<li>SMAN 4 Jember</li>
|
||||||
|
<li>Jl. Wolter Monginsidi No.24</li>
|
||||||
|
<li>Jember, Jawa Timur</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="max-w-7xl mx-auto mt-16 pt-8 border-t border-white/10 text-center">
|
||||||
|
<p class="text-[10px] font-medium uppercase tracking-[0.5em] text-slate-400">© 2026 GALANG SEFIAN ADJI - POLITEKNIK NEGERI JEMBER</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/aos@2.3.1/dist/aos.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js"></script>
|
||||||
|
<script>
|
||||||
|
AOS.init({ duration: 1000, once: true });
|
||||||
|
new Swiper(".heroSwiper", {
|
||||||
|
pagination: { el: ".swiper-pagination", clickable: true },
|
||||||
|
autoplay: { delay: 4000 },
|
||||||
|
loop: true,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,211 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="id">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Panduan Sistem - ASGARD</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;700;800&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
body { font-family: 'Plus Jakarta Sans', sans-serif; background-color: #f8fafc; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="flex flex-col min-h-screen">
|
||||||
|
|
||||||
|
<nav class="bg-white border-b border-slate-100 px-6 py-4 mb-10 relative z-50">
|
||||||
|
<div class="max-w-7xl mx-auto flex justify-between items-center">
|
||||||
|
<a href="{{ url('/') }}" class="beranda-link flex items-center gap-4 group">
|
||||||
|
<div class="w-10 h-10 bg-emerald-100 rounded-xl flex items-center justify-center text-emerald-600 font-bold transition-all duration-300 group-hover:bg-emerald-600 group-hover:text-white">AS</div>
|
||||||
|
<span class="font-black text-xl tracking-tighter text-emerald-950 uppercase italic">ASGARD.</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-8">
|
||||||
|
<div class="hidden md:flex gap-8 text-[11px] font-bold uppercase tracking-widest text-slate-400">
|
||||||
|
<a href="{{ url('/') }}" class="beranda-link hover:text-emerald-600 transition-colors">Beranda</a>
|
||||||
|
<a href="{{ route('panduan') }}" class="text-emerald-600">Panduan</a>
|
||||||
|
</div>
|
||||||
|
<a href="{{ route('login') }}" id="login-btn" class="bg-emerald-600 text-white text-[11px] font-bold uppercase tracking-widest px-6 py-3 rounded-xl hover:bg-emerald-700 hover:-translate-y-0.5 transition-all duration-300 shadow-lg shadow-emerald-600/30">
|
||||||
|
Masuk Sistem →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="max-w-4xl mx-auto px-6 flex-grow mb-20 w-full relative z-10">
|
||||||
|
|
||||||
|
<div class="mb-12 text-center md:text-left">
|
||||||
|
<div class="inline-flex items-center gap-2 bg-slate-50 text-emerald-700 px-4 py-2 rounded-full font-bold text-xs uppercase tracking-widest mb-4 border border-slate-200">
|
||||||
|
<span>📚</span> Informasi Terbuka
|
||||||
|
</div>
|
||||||
|
<h1 class="text-4xl md:text-5xl font-black text-slate-900 tracking-tight mb-4">Panduan Penggunaan ASGARD</h1>
|
||||||
|
<p class="text-slate-500 text-lg leading-relaxed">
|
||||||
|
ASGARD hadir untuk membantu memetakan karir masa depan siswa SMAN 4 Jember. Pelajari 9 langkah alur kerjanya di bawah ini.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-5">
|
||||||
|
|
||||||
|
<div class="group bg-white border border-slate-200 p-8 rounded-3xl flex flex-col md:flex-row gap-6 transition-all duration-300 hover:border-emerald-300 hover:shadow-xl hover:shadow-emerald-900/5 hover:-translate-y-1 cursor-default">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<div class="w-14 h-14 bg-slate-50 border border-slate-100 text-slate-400 rounded-2xl flex items-center justify-center text-2xl font-black transition-all duration-300 group-hover:bg-emerald-600 group-hover:border-emerald-600 group-hover:text-white group-hover:shadow-lg group-hover:shadow-emerald-600/30">1</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl font-bold text-slate-800 mb-2 transition-colors duration-300 group-hover:text-emerald-800">Pendataan oleh Guru BK (Admin)</h3>
|
||||||
|
<p class="text-slate-500 leading-relaxed transition-colors duration-300 group-hover:text-slate-600">
|
||||||
|
Siswa tidak perlu melakukan registrasi akun secara mandiri. Data dasar siswa dan pembuatan akun akan dilakukan sepenuhnya oleh Guru BK (Admin) ke dalam sistem untuk menjaga validitas data.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="group bg-white border border-slate-200 p-8 rounded-3xl flex flex-col md:flex-row gap-6 transition-all duration-300 hover:border-emerald-300 hover:shadow-xl hover:shadow-emerald-900/5 hover:-translate-y-1 cursor-default">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<div class="w-14 h-14 bg-slate-50 border border-slate-100 text-slate-400 rounded-2xl flex items-center justify-center text-2xl font-black transition-all duration-300 group-hover:bg-emerald-600 group-hover:border-emerald-600 group-hover:text-white group-hover:shadow-lg group-hover:shadow-emerald-600/30">2</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl font-bold text-slate-800 mb-2 transition-colors duration-300 group-hover:text-emerald-800">Akses & Login Akun Siswa</h3>
|
||||||
|
<p class="text-slate-500 leading-relaxed transition-colors duration-300 group-hover:text-slate-600">
|
||||||
|
Siswa dapat masuk ke sistem melalui menu <strong>Login</strong> menggunakan kredensial (seperti email/NISN) dan kata sandi yang telah diberikan oleh Guru BK.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="group bg-white border border-slate-200 p-8 rounded-3xl flex flex-col md:flex-row gap-6 transition-all duration-300 hover:border-emerald-300 hover:shadow-xl hover:shadow-emerald-900/5 hover:-translate-y-1 cursor-default">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<div class="w-14 h-14 bg-slate-50 border border-slate-100 text-slate-400 rounded-2xl flex items-center justify-center text-2xl font-black transition-all duration-300 group-hover:bg-emerald-600 group-hover:border-emerald-600 group-hover:text-white group-hover:shadow-lg group-hover:shadow-emerald-600/30">3</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl font-bold text-slate-800 mb-2 transition-colors duration-300 group-hover:text-emerald-800">Pengisian Data Ekonomi Keluarga</h3>
|
||||||
|
<p class="text-slate-500 leading-relaxed transition-colors duration-300 group-hover:text-slate-600">
|
||||||
|
Langkah pertama setelah masuk adalah menginput rentang penghasilan (gaji) orang tua. Sistem membutuhkan data ini untuk memetakan kelompok kemampuan ekonomi agar rekomendasi jurusan sesuai dengan kesiapan finansial.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="group bg-white border border-slate-200 p-8 rounded-3xl flex flex-col md:flex-row gap-6 transition-all duration-300 hover:border-emerald-300 hover:shadow-xl hover:shadow-emerald-900/5 hover:-translate-y-1 cursor-default">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<div class="w-14 h-14 bg-slate-50 border border-slate-100 text-slate-400 rounded-2xl flex items-center justify-center text-2xl font-black transition-all duration-300 group-hover:bg-emerald-600 group-hover:border-emerald-600 group-hover:text-white group-hover:shadow-lg group-hover:shadow-emerald-600/30">4</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl font-bold text-slate-800 mb-2 transition-colors duration-300 group-hover:text-emerald-800">Pengisian Kuesioner Minat & Bakat</h3>
|
||||||
|
<p class="text-slate-500 leading-relaxed transition-colors duration-300 group-hover:text-slate-600">
|
||||||
|
Selanjutnya, siswa diwajibkan untuk mengisi instrumen kuesioner psikologi untuk memetakan kecenderungan minat dan bakat secara jujur. Data ini dijamin kerahasiaannya.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="group bg-white border border-slate-200 p-8 rounded-3xl flex flex-col md:flex-row gap-6 transition-all duration-300 hover:border-emerald-300 hover:shadow-xl hover:shadow-emerald-900/5 hover:-translate-y-1 cursor-default">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<div class="w-14 h-14 bg-slate-50 border border-slate-100 text-slate-400 rounded-2xl flex items-center justify-center text-2xl font-black transition-all duration-300 group-hover:bg-emerald-600 group-hover:border-emerald-600 group-hover:text-white group-hover:shadow-lg group-hover:shadow-emerald-600/30">5</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl font-bold text-slate-800 mb-2 transition-colors duration-300 group-hover:text-emerald-800">Pengisian Form Nilai Rapor</h3>
|
||||||
|
<p class="text-slate-500 leading-relaxed transition-colors duration-300 group-hover:text-slate-600">
|
||||||
|
Tahap input terakhir adalah mengisi rata-rata nilai dari 15 mata pelajaran selama semester 1 hingga semester 5 dengan akurat sebagai indikator performa akademik utama.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="group bg-white border border-slate-200 p-8 rounded-3xl flex flex-col md:flex-row gap-6 transition-all duration-300 hover:border-emerald-300 hover:shadow-xl hover:shadow-emerald-900/5 hover:-translate-y-1 cursor-default">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<div class="w-14 h-14 bg-slate-50 border border-slate-100 text-slate-400 rounded-2xl flex items-center justify-center text-2xl font-black transition-all duration-300 group-hover:bg-emerald-600 group-hover:border-emerald-600 group-hover:text-white group-hover:shadow-lg group-hover:shadow-emerald-600/30">6</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl font-bold text-slate-800 mb-2 transition-colors duration-300 group-hover:text-emerald-800 flex items-center gap-2">
|
||||||
|
Pemrosesan Algoritma Random Forest <span class="text-xl group-hover:animate-pulse">🤖</span>
|
||||||
|
</h3>
|
||||||
|
<p class="text-slate-500 leading-relaxed transition-colors duration-300 group-hover:text-slate-600">
|
||||||
|
Sistem mulai bekerja menganalisis seluruh variabel data. Algoritma <em>Machine Learning</em> akan mencocokkan profilmu dengan database historis alumni SMAPA yang telah teruji tingkat kesuksesannya.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="group bg-white border border-slate-200 p-8 rounded-3xl flex flex-col md:flex-row gap-6 transition-all duration-300 hover:border-emerald-300 hover:shadow-xl hover:shadow-emerald-900/5 hover:-translate-y-1 cursor-default">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<div class="w-14 h-14 bg-slate-50 border border-slate-100 text-slate-400 rounded-2xl flex items-center justify-center text-2xl font-black transition-all duration-300 group-hover:bg-emerald-600 group-hover:border-emerald-600 group-hover:text-white group-hover:shadow-lg group-hover:shadow-emerald-600/30">7</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl font-bold text-slate-800 mb-2 transition-colors duration-300 group-hover:text-emerald-800">Penerbitan Hasil Rekomendasi</h3>
|
||||||
|
<p class="text-slate-500 leading-relaxed transition-colors duration-300 group-hover:text-slate-600">
|
||||||
|
Sistem akan menampilkan hasil berupa daftar program studi atau jurusan di Perguruan Tinggi yang memiliki persentase kecocokan paling tinggi dengan profil pribadimu.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="group bg-white border border-slate-200 p-8 rounded-3xl flex flex-col md:flex-row gap-6 transition-all duration-300 hover:border-emerald-300 hover:shadow-xl hover:shadow-emerald-900/5 hover:-translate-y-1 cursor-default">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<div class="w-14 h-14 bg-slate-50 border border-slate-100 text-slate-400 rounded-2xl flex items-center justify-center text-2xl font-black transition-all duration-300 group-hover:bg-emerald-600 group-hover:border-emerald-600 group-hover:text-white group-hover:shadow-lg group-hover:shadow-emerald-600/30">8</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl font-bold text-slate-800 mb-2 transition-colors duration-300 group-hover:text-emerald-800">Eksplorasi Prospek Karir</h3>
|
||||||
|
<p class="text-slate-500 leading-relaxed transition-colors duration-300 group-hover:text-slate-600">
|
||||||
|
Kamu tidak hanya mendapat nama jurusan, tapi juga dapat melihat informasi lengkap terkait peluang kerja, jalur profesi, dan relevansi industri dari jurusan yang direkomendasikan tersebut.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="group bg-white border border-slate-200 p-8 rounded-3xl flex flex-col md:flex-row gap-6 transition-all duration-300 hover:border-emerald-300 hover:shadow-xl hover:shadow-emerald-900/5 hover:-translate-y-1 cursor-default">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<div class="w-14 h-14 bg-slate-50 border border-slate-100 text-slate-400 rounded-2xl flex items-center justify-center text-2xl font-black transition-all duration-300 group-hover:bg-emerald-600 group-hover:border-emerald-600 group-hover:text-white group-hover:shadow-lg group-hover:shadow-emerald-600/30">9</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl font-bold text-slate-800 mb-2 transition-colors duration-300 group-hover:text-emerald-800">Konseling & Tindak Lanjut</h3>
|
||||||
|
<p class="text-slate-500 leading-relaxed transition-colors duration-300 group-hover:text-slate-600">
|
||||||
|
Hasil prediksi AI ini bersifat sebagai panduan objektif. Langkah terakhir, sangat disarankan untuk membawa hasil laporan sistem ini ke ruang BK untuk sesi konsultasi tatap muka guna memantapkan strategi.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="bg-[#062C22] text-white py-16 px-6 mt-auto w-full relative z-10">
|
||||||
|
<div class="max-w-7xl mx-auto grid grid-cols-1 md:grid-cols-3 gap-12">
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<span class="font-black text-2xl tracking-tighter uppercase italic">ASGARD.</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-slate-300 text-sm leading-relaxed max-w-xs">
|
||||||
|
Sistem pakar rekomendasi jurusan kuliah SMAN 4 Jember berbasis data-driven.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h5 class="font-bold text-xs uppercase tracking-widest text-emerald-400 mb-6">Informasi</h5>
|
||||||
|
<ul class="space-y-4 text-sm text-slate-300">
|
||||||
|
<li><a href="{{ route('panduan') }}" class="text-emerald-400 font-bold hover:text-white transition-colors">Panduan Sistem</a></li>
|
||||||
|
<li><a href="#" class="hover:text-white transition-colors">Kebijakan Privasi</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h5 class="font-bold text-xs uppercase tracking-widest text-emerald-400 mb-6">Sekolah</h5>
|
||||||
|
<ul class="space-y-4 text-sm text-slate-300">
|
||||||
|
<li>SMAN 4 Jember</li>
|
||||||
|
<li>Jl. Wolter Monginsidi No.24</li>
|
||||||
|
<li>Jember, Jawa Timur</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="max-w-7xl mx-auto mt-16 pt-8 border-t border-white/10 text-center">
|
||||||
|
<p class="text-[10px] font-medium uppercase tracking-[0.5em] text-slate-400">© 2026 GALANG SEFIAN ADJI - POLITEKNIK NEGERI JEMBER</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const token = localStorage.getItem('access_token');
|
||||||
|
const berandaLinks = document.querySelectorAll('.beranda-link');
|
||||||
|
const loginBtn = document.getElementById('login-btn');
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
berandaLinks.forEach(link => {
|
||||||
|
link.href = "{{ route('student.profile') }}";
|
||||||
|
});
|
||||||
|
|
||||||
|
if (loginBtn) {
|
||||||
|
loginBtn.innerText = "Ke Dashboard →";
|
||||||
|
loginBtn.href = "{{ route('student.profile') }}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,256 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="id">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>ASGARD - Dashboard Siswa</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;700;800&display=swap" rel="stylesheet">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
<style>
|
||||||
|
body { font-family: 'Plus Jakarta Sans', sans-serif; background-color: #f8fafc; }
|
||||||
|
.card-shadow { box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.02); }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="flex flex-col min-h-screen">
|
||||||
|
|
||||||
|
<nav class="bg-white border-b border-slate-100 px-6 py-4 mb-10">
|
||||||
|
<div class="max-w-7xl mx-auto flex justify-between items-center">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="w-10 h-10 bg-emerald-100 rounded-xl flex items-center justify-center text-emerald-600 font-bold">AS</div>
|
||||||
|
<span class="font-black text-xl tracking-tighter text-emerald-950 uppercase italic">ASGARD.</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-8">
|
||||||
|
<div class="hidden md:flex gap-8 text-[11px] font-bold uppercase tracking-widest text-slate-400">
|
||||||
|
<a href="{{ route('student.profile') }}" class="text-emerald-600">Beranda</a>
|
||||||
|
<a href="{{ route('student.prediction') }}" class="hover:text-emerald-600 transition">Cek Rekomendasi</a>
|
||||||
|
<a href="#" class="hover:text-emerald-600 transition">Kontak BK</a>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="flex items-center gap-3 bg-emerald-50 px-4 py-2 rounded-full border border-emerald-100">
|
||||||
|
<div class="w-2 h-2 bg-emerald-500 rounded-full animate-pulse"></div>
|
||||||
|
<span class="text-[10px] font-bold text-emerald-700 uppercase">Galang Sefian</span>
|
||||||
|
</div>
|
||||||
|
<button onclick="handleLogout()" class="bg-red-50 hover:bg-red-100 text-red-600 border border-red-100 px-4 py-2 rounded-full transition-all">
|
||||||
|
<span class="text-[10px] font-bold uppercase tracking-widest">Logout</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="max-w-7xl mx-auto px-6 flex-grow mb-20">
|
||||||
|
<header class="mb-10">
|
||||||
|
<h1 class="text-4xl font-extrabold text-slate-900 tracking-tight">Halo, Siswa SMAPA! 👋</h1>
|
||||||
|
<p class="text-slate-500 font-medium mt-2">Senang melihat Anda kembali! Mari terus pantau masa depan Anda. ✨</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-10">
|
||||||
|
<div class="bg-white p-8 rounded-3xl card-shadow border border-slate-100">
|
||||||
|
<h4 class="text-slate-400 text-xs font-bold uppercase tracking-widest mb-4">Total Aktivitas Rekomendasi</h4>
|
||||||
|
<p id="stat-total" class="text-6xl font-black text-emerald-600 tracking-tighter">0</p>
|
||||||
|
<p class="text-[11px] text-slate-400 mt-6 font-medium leading-relaxed">Jumlah total diagnosis & prediksi jurusan kuliah kamu.</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white p-8 rounded-3xl card-shadow border border-slate-100">
|
||||||
|
<h4 class="text-slate-400 text-xs font-bold uppercase tracking-widest mb-4">Rekomendasi Terakhir</h4>
|
||||||
|
<p id="stat-last" class="text-sm font-bold text-slate-700 italic leading-relaxed">Memuat data...</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white p-8 rounded-3xl card-shadow border border-slate-100">
|
||||||
|
<h4 class="text-slate-400 text-xs font-bold uppercase tracking-widest mb-4">Aktivitas Terbaru</h4>
|
||||||
|
<div id="activity-list" class="space-y-3 py-2">
|
||||||
|
<p class="text-xs text-slate-300 italic">Belum ada aktivitas baru.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white p-10 rounded-[2.5rem] card-shadow border border-slate-100 mb-10">
|
||||||
|
<h3 class="text-lg font-black text-emerald-950 mb-8 uppercase italic tracking-wider">Akses Cepat</h3>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
<a href="{{ route('student.prediction') }}" class="flex items-center p-6 bg-blue-50 rounded-2xl border border-blue-100 hover:scale-[1.02] transition-all">
|
||||||
|
<div class="w-12 h-12 bg-blue-500 text-white rounded-xl flex items-center justify-center text-xl mr-5 shadow-lg shadow-blue-200">🤖</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="font-black text-blue-900 text-sm italic">Mulai Analisis Jurusan Baru</h4>
|
||||||
|
<p class="text-[10px] text-blue-600 font-bold mt-1">Dapatkan Rekomendasi AI Sekarang.</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="flex items-center p-6 bg-yellow-50 rounded-2xl border border-yellow-100 hover:scale-[1.02] transition-all">
|
||||||
|
<div class="w-12 h-12 bg-yellow-400 text-white rounded-xl flex items-center justify-center text-xl mr-5 shadow-lg shadow-yellow-200">📊</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="font-black text-yellow-900 text-sm italic">Lengkapi Data Rapor</h4>
|
||||||
|
<p class="text-[10px] text-yellow-700 font-bold mt-1">Input variabel nilai semester 1-5.</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="flex items-center p-6 bg-purple-50 rounded-2xl border border-purple-100 hover:scale-[1.02] transition-all">
|
||||||
|
<div class="w-12 h-12 bg-purple-500 text-white rounded-xl flex items-center justify-center text-xl mr-5 shadow-lg shadow-purple-200">🎓</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="font-black text-purple-900 text-sm italic">Riwayat Rekomendasi</h4>
|
||||||
|
<p class="text-[10px] text-purple-600 font-bold mt-1">Pantau progres pilihan kuliah.</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white p-10 rounded-[2.5rem] card-shadow border border-slate-100 mb-10">
|
||||||
|
<h3 class="text-lg font-black text-emerald-950 mb-2 uppercase italic tracking-wider">Tren Prestasi Akademik Anda</h3>
|
||||||
|
<p class="text-xs text-slate-400 font-medium italic mb-10">Grafik ini menunjukkan perkembangan rata-rata nilai Anda seiring waktu.</p>
|
||||||
|
<div id="chart-container" class="h-80 w-full">
|
||||||
|
<canvas id="trendChart"></canvas>
|
||||||
|
</div>
|
||||||
|
<p id="chart-placeholder" class="hidden text-center text-slate-400 italic text-sm py-20">Input minimal 2 sesi rekomendasi untuk melihat tren di sini.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-indigo-600 p-12 rounded-[3.5rem] text-white relative overflow-hidden shadow-2xl mb-16">
|
||||||
|
<div class="relative z-10">
|
||||||
|
<div class="flex items-center gap-3 mb-8">
|
||||||
|
<span class="text-3xl">🎓</span>
|
||||||
|
<h2 class="text-3xl font-black italic tracking-tighter uppercase leading-none">Masa Depan Anda Prioritas Kami</h2>
|
||||||
|
</div>
|
||||||
|
<p class="text-indigo-100 max-w-3xl mb-12 leading-relaxed font-medium">
|
||||||
|
Tahukah kamu? <span class="font-black text-white italic underline">87% mahasiswa merasa salah jurusan</span>. ASGARD hadir membantu siswa SMAN 4 Jember menghindari risiko tersebut melalui analisis profil yang objektif menggunakan Random Forest.
|
||||||
|
</p>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div class="bg-white/10 backdrop-blur-md p-8 rounded-[2rem] border border-white/20">
|
||||||
|
<h4 class="font-black text-sm uppercase italic mb-3 tracking-widest">Anti-Blacklist SNBP</h4>
|
||||||
|
<p class="text-xs text-indigo-100 leading-relaxed italic">Hindari risiko sekolah masuk blacklist perguruan tinggi karena pembatalan kelulusan.</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white/10 backdrop-blur-md p-8 rounded-[2rem] border border-white/20">
|
||||||
|
<h4 class="font-black text-sm uppercase italic mb-3 tracking-widest">Prospek Karir AI</h4>
|
||||||
|
<p class="text-xs text-indigo-100 leading-relaxed italic">Dapatkan informasi jalur karir yang dapat ditempuh sesuai rekomendasi jurusan.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="mt-12 text-center text-[10px] font-black uppercase tracking-[0.2em] italic opacity-80 border-t border-white/10 pt-8 text-white">Penting untuk berkonsultasi dengan Guru BK setelah mendapatkan hasil rekomendasi AI ❤️</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="bg-[#062C22] text-white py-16 px-6">
|
||||||
|
<div class="max-w-7xl mx-auto grid grid-cols-1 md:grid-cols-3 gap-12">
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<span class="font-black text-2xl tracking-tighter uppercase italic">ASGARD.</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-slate-300 text-sm leading-relaxed max-w-xs">
|
||||||
|
Sistem pakar rekomendasi jurusan kuliah SMAN 4 Jember berbasis data-driven.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h5 class="font-bold text-xs uppercase tracking-widest text-emerald-400 mb-6">Informasi</h5>
|
||||||
|
<ul class="space-y-4 text-sm text-slate-300">
|
||||||
|
<li><a href="{{ route('panduan') }}" class="hover:text-white transition">Panduan Sistem</a></li>
|
||||||
|
<li><a href="#" class="hover:text-white transition">Kebijakan Privasi</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h5 class="font-bold text-xs uppercase tracking-widest text-emerald-400 mb-6">Sekolah</h5>
|
||||||
|
<ul class="space-y-4 text-sm text-slate-300">
|
||||||
|
<li>SMAN 4 Jember</li>
|
||||||
|
<li>Jl. Wolter Monginsidi No.24</li>
|
||||||
|
<li>Jember, Jawa Timur</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="max-w-7xl mx-auto mt-16 pt-8 border-t border-white/10 text-center">
|
||||||
|
<p class="text-[10px] font-medium uppercase tracking-[0.5em] text-slate-400">© 2026 GALANG SEFIAN ADJI - POLITEKNIK NEGERI JEMBER</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
async function handleLogout() {
|
||||||
|
const token = localStorage.getItem('access_token');
|
||||||
|
if (token) {
|
||||||
|
try {
|
||||||
|
await fetch('/api/logout', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Logout error:', error);
|
||||||
|
}
|
||||||
|
localStorage.removeItem('access_token');
|
||||||
|
}
|
||||||
|
window.location.href = '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', async function() {
|
||||||
|
const token = localStorage.getItem('access_token');
|
||||||
|
if (!token) {
|
||||||
|
window.location.href = '/login';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/student/dashboard-stats', {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
if (result.success) {
|
||||||
|
const data = result.data;
|
||||||
|
|
||||||
|
// Update Statistik
|
||||||
|
document.getElementById('stat-total').innerText = data.total_aktivitas;
|
||||||
|
document.getElementById('stat-last').innerText = data.rekomendasi_terakhir;
|
||||||
|
|
||||||
|
// Update List Aktivitas
|
||||||
|
const listContainer = document.getElementById('activity-list');
|
||||||
|
if (data.aktivitas_terbaru.length > 0) {
|
||||||
|
listContainer.innerHTML = data.aktivitas_terbaru.map(act => `
|
||||||
|
<div class="flex items-center justify-between bg-slate-50 p-3 rounded-xl border border-slate-100">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="text-lg">📋</span>
|
||||||
|
<span class="text-[11px] font-bold text-slate-700 uppercase tracking-tight">${act.jurusan}</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-[9px] font-black text-slate-400 uppercase italic">${act.tanggal}</span>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render Chart
|
||||||
|
const tren = data.tren_nilai;
|
||||||
|
if (tren.length >= 1) {
|
||||||
|
const ctx = document.getElementById('trendChart').getContext('2d');
|
||||||
|
new Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: tren.map(t => t.label),
|
||||||
|
datasets: [{
|
||||||
|
label: 'Rata-rata Nilai',
|
||||||
|
data: tren.map(t => t.nilai),
|
||||||
|
borderColor: '#10b981',
|
||||||
|
backgroundColor: 'rgba(16, 185, 129, 0.1)',
|
||||||
|
fill: true,
|
||||||
|
tension: 0.4,
|
||||||
|
pointRadius: 6,
|
||||||
|
pointBackgroundColor: '#fff',
|
||||||
|
pointBorderColor: '#10b981',
|
||||||
|
pointBorderWidth: 3
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: { legend: { display: false } },
|
||||||
|
scales: {
|
||||||
|
y: { min: 0, max: 100, grid: { display: false } },
|
||||||
|
x: { grid: { display: false } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
document.getElementById('trendChart').classList.add('hidden');
|
||||||
|
document.getElementById('chart-placeholder').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Gagal memuat data dashboard:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,147 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="id">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Hasil Analisis - ASGARD</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;700;800&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
body { font-family: 'Plus Jakarta Sans', sans-serif; background-color: #f8fafc; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="flex flex-col min-h-screen">
|
||||||
|
|
||||||
|
<nav class="bg-white border-b border-slate-100 px-6 py-4 mb-10">
|
||||||
|
<div class="max-w-7xl mx-auto flex justify-between items-center">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="w-10 h-10 bg-emerald-100 rounded-xl flex items-center justify-center text-emerald-600 font-bold">AS</div>
|
||||||
|
<span class="font-black text-xl tracking-tighter text-emerald-950 uppercase italic">ASGARD.</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<a href="{{ route('student.prediction') }}"
|
||||||
|
class="text-sm font-semibold text-slate-600 hover:text-emerald-600 transition">
|
||||||
|
Kembali ke Form
|
||||||
|
</a>
|
||||||
|
<button onclick="handleLogout()"
|
||||||
|
class="bg-red-50 hover:bg-red-100 text-red-600 border border-red-100 px-4 py-2 rounded-full transition-all">
|
||||||
|
<span class="text-[10px] font-bold uppercase tracking-widest">Logout</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main class="flex-grow flex items-center justify-center px-6">
|
||||||
|
<div class="max-w-xl w-full bg-white p-10 rounded-3xl shadow-xl text-center border border-slate-100 relative overflow-hidden">
|
||||||
|
<div class="absolute -top-10 -right-10 w-32 h-32 bg-emerald-50 rounded-full blur-2xl"></div>
|
||||||
|
<div class="absolute -bottom-10 -left-10 w-32 h-32 bg-blue-50 rounded-full blur-2xl"></div>
|
||||||
|
|
||||||
|
<div class="relative z-10">
|
||||||
|
<div class="w-20 h-20 bg-emerald-100 text-emerald-600 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||||
|
<svg class="w-10 h-10" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="text-slate-500 font-bold tracking-widest text-sm mb-2 uppercase">Rekomendasi AI Engine</h2>
|
||||||
|
<h1 class="text-4xl font-extrabold text-slate-800 mb-6 leading-tight">
|
||||||
|
{{ $riwayat->hasil_rekomendasi_jurusan }}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="bg-slate-50 p-6 rounded-2xl mb-8 border border-slate-100 text-left">
|
||||||
|
<h3 class="font-bold text-slate-800 mb-2">Ringkasan Analisis</h3>
|
||||||
|
<p class="text-slate-600 text-sm leading-relaxed mb-4">
|
||||||
|
Berdasarkan perpaduan profil akademik siswa, skor minat bakat RIASEC, dan kondisi ekonomi keluarga,
|
||||||
|
sistem merekomendasikan jurusan ini sebagai pilihan yang paling sesuai untuk Anda.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3 class="font-bold text-slate-800 mb-2">Peluang Karier</h3>
|
||||||
|
<p class="text-slate-600 text-sm leading-relaxed">
|
||||||
|
{{ $prospekKerja ?? 'Informasi prospek karier belum tersedia untuk jurusan ini.' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col sm:flex-row gap-3 justify-center">
|
||||||
|
<a href="{{ route('student.prediction') }}"
|
||||||
|
class="inline-block bg-slate-800 hover:bg-slate-900 text-white font-bold py-3 px-8 rounded-xl transition shadow-lg">
|
||||||
|
Analisis Ulang
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="{{ route('student.profile') }}"
|
||||||
|
class="inline-block bg-emerald-600 hover:bg-emerald-700 text-white font-bold py-3 px-8 rounded-xl transition shadow-lg">
|
||||||
|
Kembali ke Dashboard
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="bg-slate-900 text-white mt-20 py-12 px-6">
|
||||||
|
<div class="max-w-7xl mx-auto flex justify-between items-center text-center">
|
||||||
|
<p class="text-[10px] font-medium uppercase tracking-[0.2em] text-slate-500 w-full">
|
||||||
|
© 2026 ASGARD SMAN 4 JEMBER. All Rights Reserved.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function getAccessToken() {
|
||||||
|
return localStorage.getItem('access_token');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUserRole() {
|
||||||
|
return localStorage.getItem('user_role');
|
||||||
|
}
|
||||||
|
|
||||||
|
function redirectByRole(role) {
|
||||||
|
if (role === 'admin') {
|
||||||
|
window.location.href = "{{ route('admin.dashboard') }}";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (role === 'siswa') {
|
||||||
|
window.location.href = "{{ route('student.profile') }}";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.location.href = "{{ route('login') }}";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleLogout() {
|
||||||
|
const token = getAccessToken();
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
try {
|
||||||
|
await fetch('/api/logout', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Logout error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.removeItem('access_token');
|
||||||
|
localStorage.removeItem('user_role');
|
||||||
|
window.location.href = "{{ route('login') }}";
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
const token = getAccessToken();
|
||||||
|
const role = getUserRole();
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
window.location.href = "{{ route('login') }}";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (role !== 'siswa') {
|
||||||
|
redirectByRole(role);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,320 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="id">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Analisis Rekomendasi - ASGARD SMAN 4 Jember</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;700;800&display=swap" rel="stylesheet">
|
||||||
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||||
|
<style>
|
||||||
|
body { font-family: 'Plus Jakarta Sans', sans-serif; background-color: #f8fafc; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="flex flex-col min-h-screen">
|
||||||
|
|
||||||
|
<nav class="bg-white border-b border-slate-100 px-6 py-4 mb-10">
|
||||||
|
<div class="max-w-7xl mx-auto flex justify-between items-center">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="w-10 h-10 bg-emerald-100 rounded-xl flex items-center justify-center text-emerald-600 font-bold">AS</div>
|
||||||
|
<span class="font-black text-xl tracking-tighter text-emerald-950 uppercase italic">ASGARD.</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<a href="{{ route('student.profile') }}"
|
||||||
|
class="text-sm font-semibold text-slate-600 hover:text-emerald-600 transition">
|
||||||
|
Dashboard
|
||||||
|
</a>
|
||||||
|
<button onclick="handleLogout()"
|
||||||
|
class="bg-red-50 hover:bg-red-100 text-red-600 border border-red-100 px-4 py-2 rounded-full transition-all">
|
||||||
|
<span class="text-[10px] font-bold uppercase tracking-widest">Logout</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="max-w-4xl mx-auto px-6 pb-20 w-full">
|
||||||
|
<h2 class="text-3xl font-extrabold text-slate-800 mb-2">Formulir Rekomendasi Jurusan</h2>
|
||||||
|
<p class="text-slate-500 mb-8">Lengkapi data nilai dan jawab kuesioner di bawah ini dengan jujur.</p>
|
||||||
|
|
||||||
|
<div id="alert-box" class="hidden mb-6 rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700"></div>
|
||||||
|
|
||||||
|
<div class="bg-white p-6 rounded-2xl shadow-sm border border-slate-100 mb-8">
|
||||||
|
<h3 class="font-bold text-lg mb-4 text-emerald-700">1. Data Nilai Rapor (Skala 0-100)</h3>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div><label class="text-sm font-semibold">Agama</label><input type="number" id="agama" min="0" max="100" class="w-full border rounded-lg p-2 mt-1" value="85"></div>
|
||||||
|
<div><label class="text-sm font-semibold">PKN</label><input type="number" id="pkn" min="0" max="100" class="w-full border rounded-lg p-2 mt-1" value="85"></div>
|
||||||
|
<div><label class="text-sm font-semibold">Bahasa Indonesia</label><input type="number" id="bahasa_indonesia" min="0" max="100" class="w-full border rounded-lg p-2 mt-1" value="85"></div>
|
||||||
|
<div><label class="text-sm font-semibold">Bahasa Inggris</label><input type="number" id="bahasa_inggris" min="0" max="100" class="w-full border rounded-lg p-2 mt-1" value="85"></div>
|
||||||
|
<div><label class="text-sm font-semibold">Matematika</label><input type="number" id="matematika" min="0" max="100" class="w-full border rounded-lg p-2 mt-1" value="85"></div>
|
||||||
|
<div><label class="text-sm font-semibold">Fisika</label><input type="number" id="fisika" min="0" max="100" class="w-full border rounded-lg p-2 mt-1" value="85"></div>
|
||||||
|
<div><label class="text-sm font-semibold">Kimia</label><input type="number" id="kimia" min="0" max="100" class="w-full border rounded-lg p-2 mt-1" value="85"></div>
|
||||||
|
<div><label class="text-sm font-semibold">Biologi</label><input type="number" id="biologi" min="0" max="100" class="w-full border rounded-lg p-2 mt-1" value="85"></div>
|
||||||
|
<div><label class="text-sm font-semibold">Sejarah</label><input type="number" id="sejarah" min="0" max="100" class="w-full border rounded-lg p-2 mt-1" value="85"></div>
|
||||||
|
<div><label class="text-sm font-semibold">Geografi</label><input type="number" id="geografi" min="0" max="100" class="w-full border rounded-lg p-2 mt-1" value="85"></div>
|
||||||
|
<div><label class="text-sm font-semibold">Ekonomi</label><input type="number" id="ekonomi_mapel" min="0" max="100" class="w-full border rounded-lg p-2 mt-1" value="85"></div>
|
||||||
|
<div><label class="text-sm font-semibold">Sosiologi</label><input type="number" id="sosiologi" min="0" max="100" class="w-full border rounded-lg p-2 mt-1" value="85"></div>
|
||||||
|
<div><label class="text-sm font-semibold">Seni Budaya</label><input type="number" id="seni_budaya" min="0" max="100" class="w-full border rounded-lg p-2 mt-1" value="85"></div>
|
||||||
|
<div><label class="text-sm font-semibold">PJOK</label><input type="number" id="pjok" min="0" max="100" class="w-full border rounded-lg p-2 mt-1" value="85"></div>
|
||||||
|
<div><label class="text-sm font-semibold">Informatika</label><input type="number" id="informatika" min="0" max="100" class="w-full border rounded-lg p-2 mt-1" value="85"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<label class="text-sm font-semibold text-slate-700">Rata-rata Gaji Orang Tua (Nominal Rupiah)</label>
|
||||||
|
<input type="number" id="gaji_ortu" min="0" class="w-full border rounded-lg p-2 mt-1" placeholder="Contoh: 3000000" value="3000000">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white p-6 rounded-2xl shadow-sm border border-slate-100 mb-8">
|
||||||
|
<h3 class="font-bold text-lg mb-1 text-emerald-700">2. Tes Minat Bakat (RIASEC)</h3>
|
||||||
|
<p class="text-sm text-slate-500 mb-5">
|
||||||
|
Centang kegiatan yang Anda sukai. Setiap jawaban "Ya" bernilai 50 poin, dengan maksimum 100 poin per kategori.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="p-4 border border-slate-100 rounded-xl bg-slate-50">
|
||||||
|
<p class="font-bold text-slate-800 mb-2">Tipe Realistic (R)</p>
|
||||||
|
<label class="flex items-center gap-3 mb-2"><input type="checkbox" class="w-5 h-5 riasec-chk" data-type="r"> Saya suka memperbaiki barang elektronik atau mesin kendaraan.</label>
|
||||||
|
<label class="flex items-center gap-3"><input type="checkbox" class="w-5 h-5 riasec-chk" data-type="r"> Saya menyukai kegiatan fisik atau bekerja di luar ruangan.</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-4 border border-slate-100 rounded-xl bg-slate-50">
|
||||||
|
<p class="font-bold text-slate-800 mb-2">Tipe Investigative (I)</p>
|
||||||
|
<label class="flex items-center gap-3 mb-2"><input type="checkbox" class="w-5 h-5 riasec-chk" data-type="i"> Saya suka meneliti, mengamati, dan memecahkan teka-teki logika.</label>
|
||||||
|
<label class="flex items-center gap-3"><input type="checkbox" class="w-5 h-5 riasec-chk" data-type="i"> Saya tertarik mendalami teori sains, matematika, atau komputer.</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-4 border border-slate-100 rounded-xl bg-slate-50">
|
||||||
|
<p class="font-bold text-slate-800 mb-2">Tipe Artistic (A)</p>
|
||||||
|
<label class="flex items-center gap-3 mb-2"><input type="checkbox" class="w-5 h-5 riasec-chk" data-type="a"> Saya senang menggambar, mendesain, atau mengekspresikan seni.</label>
|
||||||
|
<label class="flex items-center gap-3"><input type="checkbox" class="w-5 h-5 riasec-chk" data-type="a"> Saya lebih suka pekerjaan kreatif yang tidak memiliki aturan kaku.</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-4 border border-slate-100 rounded-xl bg-slate-50">
|
||||||
|
<p class="font-bold text-slate-800 mb-2">Tipe Social (S)</p>
|
||||||
|
<label class="flex items-center gap-3 mb-2"><input type="checkbox" class="w-5 h-5 riasec-chk" data-type="s"> Saya merasa puas ketika bisa membantu, mengajar, atau merawat orang lain.</label>
|
||||||
|
<label class="flex items-center gap-3"><input type="checkbox" class="w-5 h-5 riasec-chk" data-type="s"> Saya mudah berempati dan suka bekerja dalam lingkungan masyarakat.</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-4 border border-slate-100 rounded-xl bg-slate-50">
|
||||||
|
<p class="font-bold text-slate-800 mb-2">Tipe Enterprising (E)</p>
|
||||||
|
<label class="flex items-center gap-3 mb-2"><input type="checkbox" class="w-5 h-5 riasec-chk" data-type="e"> Saya suka memimpin sebuah tim atau bernegosiasi dalam organisasi.</label>
|
||||||
|
<label class="flex items-center gap-3"><input type="checkbox" class="w-5 h-5 riasec-chk" data-type="e"> Saya pandai meyakinkan orang lain dan tertarik pada dunia bisnis.</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-4 border border-slate-100 rounded-xl bg-slate-50">
|
||||||
|
<p class="font-bold text-slate-800 mb-2">Tipe Conventional (C)</p>
|
||||||
|
<label class="flex items-center gap-3 mb-2"><input type="checkbox" class="w-5 h-5 riasec-chk" data-type="c"> Saya sangat teliti dan suka mengatur data, dokumen, atau arsip.</label>
|
||||||
|
<label class="flex items-center gap-3"><input type="checkbox" class="w-5 h-5 riasec-chk" data-type="c"> Saya lebih nyaman bekerja dengan angka, jadwal, dan aturan yang jelas.</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="btn-submit" class="w-full bg-emerald-600 hover:bg-emerald-700 text-white p-4 rounded-xl font-bold text-lg transition shadow-lg">
|
||||||
|
Jalankan AI Engine
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const alertBox = document.getElementById('alert-box');
|
||||||
|
const btnSubmit = document.getElementById('btn-submit');
|
||||||
|
|
||||||
|
function showError(message) {
|
||||||
|
alertBox.classList.remove('hidden');
|
||||||
|
alertBox.textContent = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideError() {
|
||||||
|
alertBox.classList.add('hidden');
|
||||||
|
alertBox.textContent = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNumberValue(id) {
|
||||||
|
return Number(document.getElementById(id).value || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAccessToken() {
|
||||||
|
return localStorage.getItem('access_token');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUserRole() {
|
||||||
|
return localStorage.getItem('user_role');
|
||||||
|
}
|
||||||
|
|
||||||
|
function redirectByRole(role) {
|
||||||
|
if (role === 'admin') {
|
||||||
|
window.location.href = "{{ route('admin.dashboard') }}";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (role === 'siswa') {
|
||||||
|
window.location.href = "{{ route('student.profile') }}";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.location.href = "{{ route('login') }}";
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateForm(payload) {
|
||||||
|
const scoreFields = [
|
||||||
|
'agama', 'pkn', 'bahasa_indonesia', 'bahasa_inggris', 'matematika',
|
||||||
|
'fisika', 'kimia', 'biologi', 'sejarah', 'geografi',
|
||||||
|
'ekonomi_mapel', 'sosiologi', 'seni_budaya', 'pjok', 'informatika'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const field of scoreFields) {
|
||||||
|
const value = Number(payload[field]);
|
||||||
|
if (isNaN(value) || value < 0 || value > 100) {
|
||||||
|
return `Nilai ${field.replaceAll('_', ' ')} harus berada pada rentang 0 sampai 100.`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Number(payload.gaji_ortu) < 0) {
|
||||||
|
return 'Gaji orang tua tidak boleh bernilai negatif.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleLogout() {
|
||||||
|
const token = getAccessToken();
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
try {
|
||||||
|
await fetch('/api/logout', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Logout error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.removeItem('access_token');
|
||||||
|
localStorage.removeItem('user_role');
|
||||||
|
window.location.href = "{{ route('login') }}";
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
const token = getAccessToken();
|
||||||
|
const role = getUserRole();
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
window.location.href = "{{ route('login') }}";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (role !== 'siswa') {
|
||||||
|
redirectByRole(role);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
btnSubmit.addEventListener('click', async function () {
|
||||||
|
hideError();
|
||||||
|
|
||||||
|
const token = getAccessToken();
|
||||||
|
const role = getUserRole();
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
showError('Sesi login tidak ditemukan. Silakan login kembali.');
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = "{{ route('login') }}";
|
||||||
|
}, 1200);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (role !== 'siswa') {
|
||||||
|
redirectByRole(role);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
btnSubmit.innerHTML = 'Memproses Analisis...';
|
||||||
|
btnSubmit.disabled = true;
|
||||||
|
|
||||||
|
const scores = { r: 0, i: 0, a: 0, s: 0, e: 0, c: 0 };
|
||||||
|
document.querySelectorAll('.riasec-chk:checked').forEach(chk => {
|
||||||
|
scores[chk.dataset.type] += 50;
|
||||||
|
});
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
agama: getNumberValue('agama'),
|
||||||
|
pkn: getNumberValue('pkn'),
|
||||||
|
bahasa_indonesia: getNumberValue('bahasa_indonesia'),
|
||||||
|
bahasa_inggris: getNumberValue('bahasa_inggris'),
|
||||||
|
matematika: getNumberValue('matematika'),
|
||||||
|
fisika: getNumberValue('fisika'),
|
||||||
|
kimia: getNumberValue('kimia'),
|
||||||
|
biologi: getNumberValue('biologi'),
|
||||||
|
sejarah: getNumberValue('sejarah'),
|
||||||
|
geografi: getNumberValue('geografi'),
|
||||||
|
ekonomi_mapel: getNumberValue('ekonomi_mapel'),
|
||||||
|
sosiologi: getNumberValue('sosiologi'),
|
||||||
|
seni_budaya: getNumberValue('seni_budaya'),
|
||||||
|
pjok: getNumberValue('pjok'),
|
||||||
|
informatika: getNumberValue('informatika'),
|
||||||
|
riasec_r: scores.r,
|
||||||
|
riasec_i: scores.i,
|
||||||
|
riasec_a: scores.a,
|
||||||
|
riasec_s: scores.s,
|
||||||
|
riasec_e: scores.e,
|
||||||
|
riasec_c: scores.c,
|
||||||
|
gaji_ortu: getNumberValue('gaji_ortu')
|
||||||
|
};
|
||||||
|
|
||||||
|
const validationError = validateForm(payload);
|
||||||
|
if (validationError) {
|
||||||
|
showError(validationError);
|
||||||
|
btnSubmit.innerHTML = 'Jalankan AI Engine';
|
||||||
|
btnSubmit.disabled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/student/proses-rekomendasi', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (response.ok && result.success) {
|
||||||
|
window.location.href = result.redirect_url;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status === 401) {
|
||||||
|
showError('Sesi login berakhir. Silakan login kembali.');
|
||||||
|
localStorage.removeItem('access_token');
|
||||||
|
localStorage.removeItem('user_role');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = "{{ route('login') }}";
|
||||||
|
}, 1200);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status === 403) {
|
||||||
|
showError(result.message || 'Anda tidak memiliki akses ke halaman ini.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showError(result.message || 'Terjadi kesalahan saat memproses rekomendasi.');
|
||||||
|
} catch (error) {
|
||||||
|
showError('Kesalahan jaringan. Pastikan Laravel dan API Flask sedang berjalan.');
|
||||||
|
} finally {
|
||||||
|
btnSubmit.innerHTML = 'Jalankan AI Engine';
|
||||||
|
btnSubmit.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Controllers\Api\AuthController;
|
||||||
|
use App\Http\Controllers\Api\Admin\SiswaController;
|
||||||
|
use App\Http\Controllers\Api\Admin\JurusanController;
|
||||||
|
use App\Http\Controllers\Api\Admin\GuruController;
|
||||||
|
use App\Http\Controllers\Api\Admin\AlumniController;
|
||||||
|
use App\Http\Controllers\Api\Admin\DashboardAdminController;
|
||||||
|
use App\Http\Controllers\Api\Admin\RiwayatController;
|
||||||
|
use App\Http\Controllers\Api\Admin\ProspekKerjaController;
|
||||||
|
use App\Http\Controllers\Api\Student\RecommendationApiController;
|
||||||
|
use App\Http\Controllers\Api\Student\DashboardStudentController;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
|
Route::post('/login', [AuthController::class, 'login']);
|
||||||
|
|
||||||
|
Route::middleware('auth:sanctum')->group(function () {
|
||||||
|
|
||||||
|
Route::middleware('role:admin')->prefix('admin')->group(function () {
|
||||||
|
Route::apiResource('siswa', SiswaController::class);
|
||||||
|
Route::apiResource('jurusan', JurusanController::class);
|
||||||
|
Route::apiResource('guru', GuruController::class);
|
||||||
|
Route::apiResource('alumni', AlumniController::class);
|
||||||
|
|
||||||
|
Route::apiResource('prospek-kerja', ProspekKerjaController::class);
|
||||||
|
|
||||||
|
Route::get('/dashboard-stats', [DashboardAdminController::class, 'index']);
|
||||||
|
|
||||||
|
Route::get('/riwayat', [RiwayatController::class, 'index']);
|
||||||
|
Route::get('/riwayat/{id}', [RiwayatController::class, 'show']);
|
||||||
|
});
|
||||||
|
|
||||||
|
Route::middleware('role:siswa')->prefix('student')->group(function () {
|
||||||
|
Route::get('/dashboard-stats', [DashboardStudentController::class, 'index']);
|
||||||
|
Route::post('/proses-rekomendasi', [RecommendationApiController::class, 'hitungPrediksi']);
|
||||||
|
Route::get('/hasil-rekomendasi/{id}', [RecommendationApiController::class, 'getHasilRekomendasi']);
|
||||||
|
});
|
||||||
|
|
||||||
|
Route::post('/logout', [AuthController::class, 'logout']);
|
||||||
|
});
|
||||||
|
|
@ -1,7 +1,45 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use App\Http\Controllers\LandingPageController;
|
||||||
|
use App\Http\Controllers\Student\StudentDashboardController;
|
||||||
|
use App\Http\Controllers\Student\RecommendationController;
|
||||||
|
use App\Http\Controllers\Admin\AdminDashboardController;
|
||||||
|
use App\Http\Controllers\Admin\SiswaController;
|
||||||
|
use App\Http\Controllers\Admin\JurusanController;
|
||||||
|
use App\Http\Controllers\Admin\GuruController;
|
||||||
|
use App\Http\Controllers\Admin\RiwayatController;
|
||||||
|
use App\Http\Controllers\Admin\AlumniController;
|
||||||
|
use App\Http\Controllers\Admin\ProspekKerjaController;
|
||||||
|
use App\Http\Controllers\AuthWebController;
|
||||||
|
use App\Http\Controllers\PanduanController;
|
||||||
|
|
||||||
Route::get('/', function () {
|
Route::get('/', [LandingPageController::class, 'index'])->name('landingpage');
|
||||||
return view('welcome');
|
Route::get('/login', function () { return view('auth.login'); })->name('login');
|
||||||
|
|
||||||
|
Route::get('/panduan', [PanduanController::class, 'showPanduan'])->name('panduan');
|
||||||
|
|
||||||
|
Route::prefix('student')->name('student.')->group(function () {
|
||||||
|
Route::get('/profile', [StudentDashboardController::class, 'profile'])->name('profile');
|
||||||
|
Route::get('/prediction', [RecommendationController::class, 'prediction'])->name('prediction');
|
||||||
|
Route::get('/result/{id}', [RecommendationController::class, 'result'])->name('result');
|
||||||
|
Route::get('/grades', [RecommendationController::class, 'grades'])->name('grades');
|
||||||
|
Route::get('/interest', [RecommendationController::class, 'interest'])->name('interest');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Route::prefix('admin')->name('admin.')->group(function () {
|
||||||
|
Route::get('/dashboard', [AdminDashboardController::class, 'index'])->name('dashboard');
|
||||||
|
|
||||||
|
Route::resource('siswa', SiswaController::class)->only(['index', 'create', 'edit'])->names(['index' => 'siswa']);
|
||||||
|
Route::resource('alumni', AlumniController::class)->only(['index', 'create', 'edit'])->names(['index' => 'alumni']);
|
||||||
|
Route::resource('jurusan', JurusanController::class)->only(['index', 'create', 'edit'])->names(['index' => 'jurusan']);
|
||||||
|
Route::resource('guru', GuruController::class)->only(['index', 'create', 'edit'])->names(['index' => 'guru']);
|
||||||
|
|
||||||
|
Route::resource('prospek-kerja', ProspekKerjaController::class)->only(['index', 'create', 'edit'])->names(['index' => 'prospek-kerja']);
|
||||||
|
|
||||||
|
Route::get('/riwayat', [RiwayatController::class, 'index'])->name('riwayat');
|
||||||
|
|
||||||
|
Route::get('/latih-model', function() {
|
||||||
|
return view('admin.latihmodel');
|
||||||
|
})->name('latih-model');
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue