upload all changes in project sirekom

This commit is contained in:
GalangSefin 2026-07-07 10:48:13 +07:00
parent f1d9460598
commit 0d8a13d3ac
44 changed files with 3858 additions and 1672 deletions

15
.htaccess Normal file
View File

@ -0,0 +1,15 @@
<IfModule mod_rewrite.c>
<IfModule mod_negotiation.c>
Options -MultiViews -Indexes
</IfModule>
RewriteEngine On
RewriteCond %{HTTP:Authorization} .
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [L]
</IfModule>

View File

@ -16,7 +16,7 @@ public function create()
return view('admin.siswa.siswacreate');
}
public function edit($id)
public function edit($id)
{
return view('admin.siswa.siswaedit', compact('id'));
}

View File

@ -6,102 +6,195 @@
use App\Models\Alumni;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Throwable;
class AlumniController extends Controller
{
public function index(Request $request)
{
$perPage = (int) $request->input('per_page', 7);
$perPage = in_array($perPage, [5, 7]) ? $perPage : 7;
try {
$perPage = (int) $request->input('per_page', 7);
$perPage = in_array($perPage, [5, 7, 10, 25, 50, 100]) ? $perPage : 7;
$search = trim((string) $request->input('search', ''));
$search = trim((string) $request->input('search', ''));
$query = Alumni::query()->orderBy('id', 'desc');
$query = Alumni::query()->orderBy('id', 'desc');
if ($search !== '') {
$query->where(function ($q) use ($search) {
$q->where('id_alumni', 'like', "%{$search}%")
->orWhere('nama_alumni', 'like', "%{$search}%")
->orWhere('perguruan_tinggi', 'like', "%{$search}%")
->orWhere('jurusan_diambil', 'like', "%{$search}%");
});
if ($search !== '') {
$query->where(function ($q) use ($search) {
$q->where('id_alumni', 'like', "%{$search}%")
->orWhere('nama_alumni', 'like', "%{$search}%")
->orWhere('perguruan_tinggi', 'like', "%{$search}%")
->orWhere('jurusan_diambil', 'like', "%{$search}%");
});
}
$alumni = $query->paginate($perPage);
return response()->json([
'success' => true,
'message' => 'Data alumni berhasil dimuat.',
'data' => $alumni->items(),
'current_page' => $alumni->currentPage(),
'last_page' => $alumni->lastPage(),
'per_page' => $alumni->perPage(),
'total' => $alumni->total(),
'from' => $alumni->firstItem(),
'to' => $alumni->lastItem(),
], 200);
} catch (Throwable $e) {
return response()->json([
'success' => false,
'message' => 'Gagal memuat data alumni.',
'error' => $e->getMessage(),
], 500);
}
$alumni = $query->paginate($perPage);
return response()->json([
'success' => true,
'data' => $alumni->items(),
'current_page' => $alumni->currentPage(),
'last_page' => $alumni->lastPage(),
'per_page' => $alumni->perPage(),
'total' => $alumni->total(),
'from' => $alumni->firstItem(),
'to' => $alumni->lastItem(),
], 200);
}
public function store(Request $request)
{
$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.',
]);
try {
$validator = Validator::make($request->all(), [
'nama_alumni' => 'required|string|max:255',
'perguruan_tinggi' => 'required|string|max:255',
'jurusan_diambil' => 'required|string|max:255',
], [
'nama_alumni.required' => 'Nama alumni wajib diisi.',
'perguruan_tinggi.required' => 'Nama perguruan tinggi wajib diisi.',
'jurusan_diambil.required' => 'Jurusan yang diambil wajib diisi.',
]);
if ($validator->fails()) {
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
if ($validator->fails()) {
return response()->json([
'success' => false,
'message' => 'Validasi gagal.',
'errors' => $validator->errors(),
], 422);
}
$alumni = new Alumni();
$alumni->nama_alumni = $request->input('nama_alumni');
$alumni->perguruan_tinggi = $request->input('perguruan_tinggi');
$alumni->jurusan_diambil = $request->input('jurusan_diambil');
$alumni->profil_nilai = $request->input('profil_nilai');
$alumni->profil_minat = $request->input('profil_minat');
$alumni->save();
return response()->json([
'success' => true,
'message' => 'Data alumni berhasil ditambahkan.',
'data' => $alumni,
], 201);
} catch (Throwable $e) {
return response()->json([
'success' => false,
'message' => 'Terjadi kesalahan saat menyimpan data alumni.',
'error' => $e->getMessage(),
], 500);
}
}
$alumni = Alumni::create($request->all());
public function show($id)
{
try {
$alumni = Alumni::find($id);
return response()->json([
'success' => true,
'message' => 'Alumni ' . $alumni->id_alumni . ' berhasil ditambahkan.',
'data' => $alumni
], 201);
if (!$alumni) {
return response()->json([
'success' => false,
'message' => 'Data alumni tidak ditemukan.',
], 404);
}
return response()->json([
'success' => true,
'message' => 'Data alumni berhasil ditemukan.',
'data' => $alumni,
], 200);
} catch (Throwable $e) {
return response()->json([
'success' => false,
'message' => 'Terjadi kesalahan saat mengambil data alumni.',
'error' => $e->getMessage(),
], 500);
}
}
public function update(Request $request, $id)
{
$alumni = Alumni::find($id);
try {
$alumni = Alumni::find($id);
if (!$alumni) {
return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
if (!$alumni) {
return response()->json([
'success' => false,
'message' => 'Data alumni tidak ditemukan.',
], 404);
}
$validator = Validator::make($request->all(), [
'nama_alumni' => 'required|string|max:255',
'perguruan_tinggi' => 'required|string|max:255',
'jurusan_diambil' => 'required|string|max:255',
], [
'nama_alumni.required' => 'Nama alumni wajib diisi.',
'perguruan_tinggi.required' => 'Nama perguruan tinggi wajib diisi.',
'jurusan_diambil.required' => 'Jurusan yang diambil wajib diisi.',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'message' => 'Validasi gagal.',
'errors' => $validator->errors(),
], 422);
}
$alumni->nama_alumni = $request->input('nama_alumni');
$alumni->perguruan_tinggi = $request->input('perguruan_tinggi');
$alumni->jurusan_diambil = $request->input('jurusan_diambil');
$alumni->profil_nilai = $request->input('profil_nilai');
$alumni->profil_minat = $request->input('profil_minat');
$alumni->save();
return response()->json([
'success' => true,
'message' => 'Data alumni berhasil diperbarui.',
'data' => $alumni,
], 200);
} catch (Throwable $e) {
return response()->json([
'success' => false,
'message' => 'Terjadi kesalahan saat memperbarui data alumni.',
'error' => $e->getMessage(),
], 500);
}
$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);
try {
$alumni = Alumni::find($id);
if (!$alumni) {
return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
if (!$alumni) {
return response()->json([
'success' => false,
'message' => 'Data alumni tidak ditemukan.',
], 404);
}
$alumni->delete();
return response()->json([
'success' => true,
'message' => 'Data alumni berhasil dihapus.',
], 200);
} catch (Throwable $e) {
return response()->json([
'success' => false,
'message' => 'Data alumni tidak dapat dihapus karena masih digunakan atau terjadi kesalahan server.',
'error' => $e->getMessage(),
], 500);
}
$alumni->delete();
return response()->json(['success' => true, 'message' => 'Data Alumni berhasil dihapus'], 200);
}
}

View File

@ -3,40 +3,41 @@
namespace App\Http\Controllers\Api\Admin;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Models\RiwayatRekomendasi;
use App\Models\Siswa;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Schema;
use Exception;
use Throwable;
class DashboardAdminController extends Controller
{
public function index(Request $request)
{
try {
$tableName = (new RiwayatRekomendasi())->getTable();
$riwayatModel = new RiwayatRekomendasi();
$tableName = $riwayatModel->getTable();
$totalSiswa = User::whereIn('role', ['siswa', 'student'])->count();
$totalSiswa = 0;
try {
$siswaModel = new Siswa();
if (Schema::hasTable($siswaModel->getTable())) {
$totalSiswa = Siswa::count();
}
} catch (Throwable $e) {
$totalSiswa = 0;
}
if ($totalSiswa === 0) {
$totalSiswa = RiwayatRekomendasi::distinct('user_id')->count('user_id');
}
$totalRekomendasi = RiwayatRekomendasi::count();
$kolomAkurasi = null;
if (Schema::hasColumn($tableName, 'akurasi_prediksi')) {
$kolomAkurasi = 'akurasi_prediksi';
} elseif (Schema::hasColumn($tableName, 'probabilitas')) {
$kolomAkurasi = 'probabilitas';
} elseif (Schema::hasColumn($tableName, 'akurasi')) {
$kolomAkurasi = 'akurasi';
$totalSiswa = User::whereIn('role', ['siswa', 'student'])->count();
}
$totalRekomendasi = Schema::hasTable($tableName) ? RiwayatRekomendasi::count() : 0;
$kolomAkurasi = $this->getKolomAkurasi($tableName);
$rataAkurasi = null;
if ($kolomAkurasi) {
if ($kolomAkurasi && Schema::hasTable($tableName)) {
$avg = RiwayatRekomendasi::whereNotNull($kolomAkurasi)->avg($kolomAkurasi);
$rataAkurasi = $avg !== null ? round((float) $avg, 2) : null;
}
@ -45,44 +46,64 @@ public function index(Request $request)
? 'Berdasarkan rata-rata tingkat keyakinan dari seluruh hasil rekomendasi.'
: 'Belum ada nilai keyakinan prediksi yang tersimpan. Lakukan prediksi baru terlebih dahulu.';
$selectDistribusi = 'hasil_rekomendasi_jurusan as jurusan, COUNT(*) as total';
$distribusiJurusan = collect();
if ($kolomAkurasi) {
$selectDistribusi .= ', ROUND(AVG(' . $kolomAkurasi . '), 2) as rata_akurasi';
if (Schema::hasTable($tableName) && Schema::hasColumn($tableName, 'hasil_rekomendasi_jurusan')) {
$selectDistribusi = 'hasil_rekomendasi_jurusan as jurusan, COUNT(*) as total';
if ($kolomAkurasi) {
$selectDistribusi .= ', ROUND(AVG(' . $kolomAkurasi . '), 2) as rata_akurasi';
}
$distribusiJurusan = RiwayatRekomendasi::selectRaw($selectDistribusi)
->whereNotNull('hasil_rekomendasi_jurusan')
->groupBy('hasil_rekomendasi_jurusan')
->orderBy('total', 'desc')
->limit(10)
->get()
->map(function ($item) use ($kolomAkurasi) {
return [
'jurusan' => $item->jurusan,
'total' => (int) $item->total,
'rata_akurasi' => $kolomAkurasi && $item->rata_akurasi !== null
? round((float) $item->rata_akurasi, 2)
: null,
];
});
}
$distribusiJurusan = RiwayatRekomendasi::selectRaw($selectDistribusi)
->whereNotNull('hasil_rekomendasi_jurusan')
->groupBy('hasil_rekomendasi_jurusan')
->orderBy('total', 'desc')
->limit(10)
->get()
->map(function ($item) use ($kolomAkurasi) {
return [
'jurusan' => $item->jurusan,
'total' => (int) $item->total,
'rata_akurasi' => $kolomAkurasi && $item->rata_akurasi !== null
? round((float) $item->rata_akurasi, 2)
: null,
];
});
return response()->json([
'success' => true,
'message' => 'Statistik dashboard berhasil dimuat.',
'data' => [
'total_siswa' => $totalSiswa,
'rekomendasi_diproses' => $totalRekomendasi,
'rata_rata_akurasi' => $rataAkurasi,
'akurasi_status' => $akurasiStatus,
'distribusi_jurusan' => $distribusiJurusan,
]
],
], 200);
} catch (Exception $e) {
} catch (Throwable $e) {
return response()->json([
'success' => false,
'message' => 'Error: ' . $e->getMessage()
'message' => 'Gagal memuat statistik dashboard.',
'error' => $e->getMessage(),
], 500);
}
}
private function getKolomAkurasi(string $tableName): ?string
{
if (!Schema::hasTable($tableName)) {
return null;
}
foreach (['akurasi_prediksi', 'probabilitas', 'akurasi'] as $column) {
if (Schema::hasColumn($tableName, $column)) {
return $column;
}
}
return null;
}
}

View File

@ -4,98 +4,242 @@
use App\Http\Controllers\Controller;
use App\Models\Guru;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\DB;
use Throwable;
class GuruController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(Request $request)
{
$perPage = (int) $request->input('per_page', 7);
$perPage = in_array($perPage, [5, 7]) ? $perPage : 7;
try {
$perPage = (int) $request->input('per_page', 7);
$perPage = in_array($perPage, [5, 7, 10, 25]) ? $perPage : 7;
$search = trim((string) $request->input('search', ''));
$search = trim((string) $request->input('search', ''));
$query = Guru::query()->orderBy('id', 'desc');
$query = Guru::query()->with('user')->orderBy('id', 'desc');
if ($search !== '') {
$query->where(function ($q) use ($search) {
$q->where('id_guru', 'like', "%{$search}%")
->orWhere('nama_guru', 'like', "%{$search}%");
});
if ($search !== '') {
$query->where(function ($q) use ($search) {
$q->where('id_guru', 'like', "%{$search}%")
->orWhere('nama_guru', 'like', "%{$search}%");
});
}
$guru = $query->paginate($perPage);
return response()->json([
'success' => true,
'message' => 'Data guru berhasil dimuat.',
'data' => $guru->items(),
'current_page' => $guru->currentPage(),
'last_page' => $guru->lastPage(),
'per_page' => $guru->perPage(),
'total' => $guru->total(),
], 200);
} catch (Throwable $e) {
return response()->json([
'success' => false,
'message' => 'Terjadi kesalahan saat memuat data guru.',
'error' => $e->getMessage()
], 500);
}
$guru = $query->paginate($perPage);
return response()->json([
'success' => true,
'data' => $guru->items(),
'current_page' => $guru->currentPage(),
'last_page' => $guru->lastPage(),
'per_page' => $guru->perPage(),
'total' => $guru->total(),
'from' => $guru->firstItem(),
'to' => $guru->lastItem(),
], 200);
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
$validator = Validator::make($request->all(), [
'nama_guru' => 'required|string|max:255',
], [
'nama_guru.required' => 'Nama guru tidak boleh kosong.',
'username' => 'required|string|max:255|unique:users,username',
'email' => 'required|string|email|max:255|unique:users,email',
'password' => 'required|string|min:8',
]);
if ($validator->fails()) {
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
return response()->json([
'success' => false,
'message' => 'Validasi gagal.',
'errors' => $validator->errors()
], 422);
}
$guru = Guru::create($request->all());
DB::beginTransaction();
return response()->json([
'success' => true,
'message' => 'Guru ' . $guru->id_guru . ' berhasil ditambahkan.',
'data' => $guru
], 201);
try {
// Create User Akun
$user = User::create([
'name' => $request->nama_guru,
'username' => $request->username,
'email' => $request->email,
'password' => Hash::make($request->password),
'role' => 'admin',
]);
// Create Data Guru terikat dengan user_id
$guru = Guru::create([
'user_id' => $user->id,
'nama_guru' => $request->nama_guru,
]);
DB::commit();
$guru->load('user');
return response()->json([
'success' => true,
'message' => 'Data guru dan akun login berhasil disimpan.',
'data' => $guru
], 201);
} catch (Throwable $e) {
DB::rollBack();
return response()->json([
'success' => false,
'message' => 'Terjadi kesalahan saat menyimpan data guru.',
'error' => $e->getMessage()
], 500);
}
}
/**
* Display the specified resource.
*/
public function show($id)
{
try {
$guru = Guru::with('user')->find($id);
if (!$guru) {
return response()->json([
'success' => false,
'message' => 'Data guru tidak ditemukan.'
], 404);
}
return response()->json([
'success' => true,
'message' => 'Detail data guru berhasil ditemukan.',
'data' => $guru
], 200);
} catch (Throwable $e) {
return response()->json([
'success' => false,
'message' => 'Terjadi kesalahan saat mengambil detail data guru.',
'error' => $e->getMessage()
], 500);
}
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, $id)
{
$guru = Guru::find($id);
try {
$guru = Guru::find($id);
if (!$guru) {
return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
if (!$guru) {
return response()->json([
'success' => false,
'message' => 'Data guru tidak ditemukan.'
], 404);
}
$validator = Validator::make($request->all(), [
'nama_guru' => 'required|string|max:255',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'message' => 'Validasi gagal.',
'errors' => $validator->errors()
], 422);
}
DB::beginTransaction();
$guru->update([
'nama_guru' => $request->nama_guru
]);
if ($guru->user) {
$guru->user->update([
'name' => $request->nama_guru
]);
}
DB::commit();
$guru->load('user');
return response()->json([
'success' => true,
'message' => 'Data guru berhasil diperbarui.',
'data' => $guru
], 200);
} catch (Throwable $e) {
DB::rollBack();
return response()->json([
'success' => false,
'message' => 'Terjadi kesalahan saat memperbarui data guru.',
'error' => $e->getMessage()
], 500);
}
$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);
}
/**
* Remove the specified resource from storage.
*/
public function destroy($id)
{
$guru = Guru::find($id);
try {
$guru = Guru::find($id);
if (!$guru) {
return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
if (!$guru) {
return response()->json([
'success' => false,
'message' => 'Data guru tidak ditemukan.'
], 404);
}
DB::beginTransaction();
if ($guru->user_id) {
User::destroy($guru->user_id);
}
$guru->delete();
DB::commit();
return response()->json([
'success' => true,
'message' => 'Data guru dan akun login berhasil dihapus.'
], 200);
} catch (Throwable $e) {
DB::rollBack();
return response()->json([
'success' => false,
'message' => 'Data guru tidak dapat dihapus atau terjadi kesalahan server.',
'error' => $e->getMessage()
], 500);
}
$guru->delete();
return response()->json(['success' => true, 'message' => 'Data Guru berhasil dihapus'], 200);
}
}

View File

@ -6,98 +6,191 @@
use App\Models\Jurusan;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Throwable;
class JurusanController extends Controller
{
public function index(Request $request)
{
$perPage = (int) $request->input('per_page', 7);
$perPage = in_array($perPage, [5, 7]) ? $perPage : 7;
try {
$perPage = (int) $request->input('per_page', 7);
$perPage = in_array($perPage, [5, 7, 10, 25, 50, 100]) ? $perPage : 7;
$search = trim((string) $request->input('search', ''));
$search = trim((string) $request->input('search', ''));
$query = Jurusan::query()->orderBy('id', 'desc');
// Mengambil data jurusan beserta semua prospek kerja yang terkait dengannya
$query = Jurusan::with('prospeks')->orderBy('id', 'desc');
if ($search !== '') {
$query->where(function ($q) use ($search) {
$q->where('nama_jurusan', 'like', "%{$search}%")
->orWhere('rumpun', 'like', "%{$search}%")
->orWhere('prospek_karir', 'like', "%{$search}%");
});
if ($search !== '') {
$query->where(function ($q) use ($search) {
$q->where('nama_jurusan', 'like', "%{$search}%")
->orWhere('rumpun', 'like', "%{$search}%")
// Memperbaiki pencarian agar mencari ke dalam relasi tabel prospeks
->orWhereHas('prospeks', function ($relationQuery) use ($search) {
$relationQuery->where('nama_profesi', 'like', "%{$search}%");
});
});
}
$jurusan = $query->paginate($perPage);
return response()->json([
'success' => true,
'message' => 'Data jurusan berhasil dimuat.',
'data' => $jurusan->items(),
'current_page' => $jurusan->currentPage(),
'last_page' => $jurusan->lastPage(),
'per_page' => $jurusan->perPage(),
'total' => $jurusan->total(),
'from' => $jurusan->firstItem(),
'to' => $jurusan->lastItem(),
], 200);
} catch (Throwable $e) {
return response()->json([
'success' => false,
'message' => 'Terjadi kesalahan saat memuat data jurusan.',
'error' => $e->getMessage(),
], 500);
}
$jurusan = $query->paginate($perPage);
return response()->json([
'success' => true,
'data' => $jurusan->items(),
'current_page' => $jurusan->currentPage(),
'last_page' => $jurusan->lastPage(),
'per_page' => $jurusan->perPage(),
'total' => $jurusan->total(),
'from' => $jurusan->firstItem(),
'to' => $jurusan->lastItem(),
], 200);
}
public function store(Request $request)
{
$validator = Validator::make($request->all(), [
'nama_jurusan' => 'required|string|max:255',
'rumpun' => 'required|string',
]);
try {
$validator = Validator::make($request->all(), [
'nama_jurusan' => 'required|string|max:255',
'rumpun' => ['required', 'string', Rule::in(['Saintek', 'Soshum'])],
], [
'nama_jurusan.required' => 'Nama jurusan wajib diisi.',
'rumpun.required' => 'Rumpun jurusan wajib dipilih.',
'rumpun.in' => 'Rumpun hanya boleh Saintek atau Soshum.',
]);
if ($validator->fails()) {
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
if ($validator->fails()) {
return response()->json([
'success' => false,
'message' => 'Validasi gagal.',
'errors' => $validator->errors(),
], 422);
}
$jurusan = new Jurusan();
$jurusan->nama_jurusan = $request->input('nama_jurusan');
$jurusan->rumpun = $request->input('rumpun');
$jurusan->save();
return response()->json([
'success' => true,
'message' => 'Data jurusan berhasil ditambahkan.',
'data' => $jurusan,
], 201);
} catch (Throwable $e) {
return response()->json([
'success' => false,
'message' => 'Terjadi kesalahan saat menyimpan data jurusan.',
'error' => $e->getMessage(),
], 500);
}
$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);
try {
$jurusan = Jurusan::find($id);
if (!$jurusan) {
return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
if (!$jurusan) {
return response()->json([
'success' => false,
'message' => 'Data jurusan tidak ditemukan.',
], 404);
}
return response()->json([
'success' => true,
'message' => 'Data jurusan berhasil ditemukan.',
'data' => $jurusan,
], 200);
} catch (Throwable $e) {
return response()->json([
'success' => false,
'message' => 'Terjadi kesalahan saat mengambil data jurusan.',
'error' => $e->getMessage(),
], 500);
}
return response()->json(['success' => true, 'data' => $jurusan], 200);
}
public function update(Request $request, $id)
{
$jurusan = Jurusan::find($id);
try {
$jurusan = Jurusan::find($id);
if (!$jurusan) {
return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
if (!$jurusan) {
return response()->json([
'success' => false,
'message' => 'Data jurusan tidak ditemukan.',
], 404);
}
$validator = Validator::make($request->all(), [
'nama_jurusan' => [
'required',
'string',
'max:255',
Rule::unique('jurusans', 'nama_jurusan')->ignore($id),
],
'rumpun' => 'required|in:Saintek,Soshum',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'message' => 'Validasi gagal.',
'errors' => $validator->errors(),
], 422);
}
$jurusan->nama_jurusan = $request->input('nama_jurusan');
$jurusan->rumpun = $request->input('rumpun');
$jurusan->save();
return response()->json([
'success' => true,
'message' => 'Data jurusan berhasil diperbarui.',
'data' => $jurusan,
], 200);
} catch (Throwable $e) {
return response()->json([
'success' => false,
'message' => 'Terjadi kesalahan saat memperbarui data jurusan.',
'error' => $e->getMessage(),
], 500);
}
$jurusan->update($request->all());
return response()->json([
'success' => true,
'message' => 'Jurusan berhasil diperbarui',
'data' => $jurusan
], 200);
}
public function destroy($id)
{
$jurusan = Jurusan::find($id);
try {
$jurusan = Jurusan::find($id);
if (!$jurusan) {
return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
if (!$jurusan) {
return response()->json([
'success' => false,
'message' => 'Data jurusan tidak ditemukan.',
], 404);
}
$jurusan->delete();
return response()->json([
'success' => true,
'message' => 'Data jurusan berhasil dihapus.',
], 200);
} catch (Throwable $e) {
return response()->json([
'success' => false,
'message' => 'Data jurusan tidak dapat dihapus karena masih digunakan pada data lain atau terjadi kesalahan server.',
'error' => $e->getMessage(),
], 500);
}
$jurusan->delete();
return response()->json(['success' => true, 'message' => 'Jurusan berhasil dihapus'], 200);
}
}

View File

@ -6,114 +6,199 @@
use App\Models\ProspekKerja;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Throwable;
class ProspekKerjaController extends Controller
{
public function index(Request $request)
{
$perPage = (int) $request->input('per_page', 7);
$perPage = in_array($perPage, [5, 7]) ? $perPage : 7;
try {
$perPage = (int) $request->input('per_page', 7);
$perPage = in_array($perPage, [5, 7, 10, 25, 50, 100]) ? $perPage : 7;
$search = trim((string) $request->input('search', ''));
$search = trim((string) $request->input('search', ''));
$query = ProspekKerja::with('jurusan')->orderBy('id', 'desc');
$query = ProspekKerja::with('jurusan')->orderBy('id', 'desc');
if ($search !== '') {
$query->where(function ($q) use ($search) {
$q->where('nama_profesi', 'like', "%{$search}%")
->orWhere('range_gaji', 'like', "%{$search}%")
->orWhere('deskripsi_pekerjaan', 'like', "%{$search}%")
->orWhereHas('jurusan', function ($jurusan) use ($search) {
$jurusan->where('nama_jurusan', 'like', "%{$search}%");
});
});
if ($search !== '') {
$query->where(function ($q) use ($search) {
$q->where('nama_profesi', 'like', "%{$search}%")
->orWhere('range_gaji', 'like', "%{$search}%")
->orWhere('deskripsi_pekerjaan', 'like', "%{$search}%")
->orWhereHas('jurusan', function ($jurusan) use ($search) {
$jurusan->where('nama_jurusan', 'like', "%{$search}%");
});
});
}
$prospek = $query->paginate($perPage);
return response()->json([
'success' => true,
'message' => 'Data prospek kerja berhasil dimuat.',
'data' => $prospek->items(),
'current_page' => $prospek->currentPage(),
'last_page' => $prospek->lastPage(),
'per_page' => $prospek->perPage(),
'total' => $prospek->total(),
'from' => $prospek->firstItem(),
'to' => $prospek->lastItem(),
], 200);
} catch (Throwable $e) {
return response()->json([
'success' => false,
'message' => 'Gagal memuat data prospek kerja.',
'error' => $e->getMessage(),
], 500);
}
$prospek = $query->paginate($perPage);
return response()->json([
'success' => true,
'data' => $prospek->items(),
'current_page' => $prospek->currentPage(),
'last_page' => $prospek->lastPage(),
'per_page' => $prospek->perPage(),
'total' => $prospek->total(),
'from' => $prospek->firstItem(),
'to' => $prospek->lastItem(),
], 200);
}
public function store(Request $request)
{
$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',
]);
try {
$validator = Validator::make($request->all(), [
'jurusan_id' => 'required|integer|exists:jurusans,id',
'nama_profesi' => 'required|string|max:255',
'range_gaji' => 'nullable|string|max:255',
'deskripsi_pekerjaan' => 'nullable|string',
], [
'jurusan_id.required' => 'Jurusan wajib dipilih.',
'jurusan_id.exists' => 'Jurusan yang dipilih tidak ditemukan.',
'nama_profesi.required' => 'Nama profesi wajib diisi.',
]);
if ($validator->fails()) {
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
if ($validator->fails()) {
return response()->json([
'success' => false,
'message' => 'Validasi gagal.',
'errors' => $validator->errors(),
], 422);
}
$prospek = new ProspekKerja();
$prospek->jurusan_id = $request->input('jurusan_id');
$prospek->nama_profesi = $request->input('nama_profesi');
$prospek->range_gaji = $request->input('range_gaji');
$prospek->deskripsi_pekerjaan = $request->input('deskripsi_pekerjaan');
$prospek->save();
$prospek->load('jurusan');
return response()->json([
'success' => true,
'message' => 'Data prospek kerja berhasil ditambahkan.',
'data' => $prospek,
], 201);
} catch (Throwable $e) {
return response()->json([
'success' => false,
'message' => 'Terjadi kesalahan saat menyimpan data prospek kerja.',
'error' => $e->getMessage(),
], 500);
}
$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);
try {
$prospek = ProspekKerja::with('jurusan')->find($id);
if (!$prospek) {
return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
if (!$prospek) {
return response()->json([
'success' => false,
'message' => 'Data prospek kerja tidak ditemukan.',
], 404);
}
return response()->json([
'success' => true,
'message' => 'Data prospek kerja berhasil ditemukan.',
'data' => $prospek,
], 200);
} catch (Throwable $e) {
return response()->json([
'success' => false,
'message' => 'Terjadi kesalahan saat mengambil data prospek kerja.',
'error' => $e->getMessage(),
], 500);
}
return response()->json(['success' => true, 'data' => $prospek], 200);
}
public function update(Request $request, $id)
{
$prospek = ProspekKerja::find($id);
try {
$prospek = ProspekKerja::find($id);
if (!$prospek) {
return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
if (!$prospek) {
return response()->json([
'success' => false,
'message' => 'Data prospek kerja tidak ditemukan.',
], 404);
}
$validator = Validator::make($request->all(), [
'jurusan_id' => 'required|integer|exists:jurusans,id',
'nama_profesi' => 'required|string|max:255',
'range_gaji' => 'nullable|string|max:255',
'deskripsi_pekerjaan' => 'nullable|string',
], [
'jurusan_id.required' => 'Jurusan wajib dipilih.',
'jurusan_id.exists' => 'Jurusan yang dipilih tidak ditemukan.',
'nama_profesi.required' => 'Nama profesi wajib diisi.',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'message' => 'Validasi gagal.',
'errors' => $validator->errors(),
], 422);
}
$prospek->jurusan_id = $request->input('jurusan_id');
$prospek->nama_profesi = $request->input('nama_profesi');
$prospek->range_gaji = $request->input('range_gaji');
$prospek->deskripsi_pekerjaan = $request->input('deskripsi_pekerjaan');
$prospek->save();
$prospek->load('jurusan');
return response()->json([
'success' => true,
'message' => 'Data prospek kerja berhasil diperbarui.',
'data' => $prospek,
], 200);
} catch (Throwable $e) {
return response()->json([
'success' => false,
'message' => 'Terjadi kesalahan saat memperbarui data prospek kerja.',
'error' => $e->getMessage(),
], 500);
}
$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);
try {
$prospek = ProspekKerja::find($id);
if (!$prospek) {
return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
if (!$prospek) {
return response()->json([
'success' => false,
'message' => 'Data prospek kerja tidak ditemukan.',
], 404);
}
$prospek->delete();
return response()->json([
'success' => true,
'message' => 'Data prospek kerja berhasil dihapus.',
], 200);
} catch (Throwable $e) {
return response()->json([
'success' => false,
'message' => 'Data prospek kerja tidak dapat dihapus karena masih digunakan atau terjadi kesalahan server.',
'error' => $e->getMessage(),
], 500);
}
$prospek->delete();
return response()->json(['success' => true, 'message' => 'Prospek Kerja berhasil dihapus'], 200);
}
}

View File

@ -6,7 +6,7 @@
use App\Models\RiwayatRekomendasi;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Schema;
use Exception;
use Throwable;
class RiwayatController extends Controller
{
@ -14,7 +14,7 @@ public function index(Request $request)
{
try {
$perPage = (int) $request->input('per_page', 7);
$perPage = in_array($perPage, [5, 7]) ? $perPage : 7;
$perPage = in_array($perPage, [5, 7, 10, 25, 50, 100]) ? $perPage : 7;
$search = trim((string) $request->input('search', ''));
@ -49,6 +49,7 @@ public function index(Request $request)
return response()->json([
'success' => true,
'message' => 'Data riwayat berhasil dimuat.',
'data' => $riwayat->items(),
'current_page' => $riwayat->currentPage(),
'last_page' => $riwayat->lastPage(),
@ -57,11 +58,11 @@ public function index(Request $request)
'from' => $riwayat->firstItem(),
'to' => $riwayat->lastItem(),
], 200);
} catch (Exception $e) {
} catch (Throwable $e) {
return response()->json([
'success' => false,
'message' => 'Error: ' . $e->getMessage()
'message' => 'Gagal memuat data riwayat.',
'error' => $e->getMessage(),
], 500);
}
}
@ -69,10 +70,18 @@ public function index(Request $request)
public function show($id)
{
try {
$detail = RiwayatRekomendasi::with('user:id,name,email')->findOrFail($id);
$detail = RiwayatRekomendasi::with('user:id,name,email')->find($id);
if (!$detail) {
return response()->json([
'success' => false,
'message' => 'Data detail riwayat tidak ditemukan.',
], 404);
}
return response()->json([
'success' => true,
'message' => 'Detail riwayat berhasil ditemukan.',
'data' => [
'id' => $detail->id,
'tanggal' => $detail->created_at ? $detail->created_at->format('d M Y H:i:s') : '-',
@ -85,13 +94,20 @@ public function show($id)
'kondisi_ekonomi' => $this->getLabelEkonomi($detail->gaji_ortu),
'gaji_ortu' => $detail->gaji_ortu,
'nilai_ringkas' => [
'Matematika' => $detail->matematika,
'Agama' => $detail->agama,
'PKN' => $detail->pkn,
'B. Indonesia' => $detail->bahasa_indonesia,
'B. Inggris' => $detail->bahasa_inggris,
'Matematika' => $detail->matematika,
'Fisika' => $detail->fisika,
'Kimia' => $detail->kimia,
'Biologi' => $detail->biologi,
'Sejarah' => $detail->sejarah,
'Geografi' => $detail->geografi,
'Ekonomi' => $detail->ekonomi_mapel,
'Sosiologi' => $detail->sosiologi,
'Seni Budaya' => $detail->seni_budaya,
'PJOK' => $detail->pjok,
'Informatika' => $detail->informatika,
],
'riasec' => [
@ -102,14 +118,41 @@ public function show($id)
'Enterprising' => $detail->riasec_e,
'Conventional' => $detail->riasec_c,
],
]
],
], 200);
} catch (Exception $e) {
} catch (Throwable $e) {
return response()->json([
'success' => false,
'message' => 'Data detail tidak ditemukan: ' . $e->getMessage()
], 404);
'message' => 'Terjadi kesalahan saat mengambil detail riwayat.',
'error' => $e->getMessage(),
], 500);
}
}
public function destroy($id)
{
try {
$riwayat = RiwayatRekomendasi::find($id);
if (!$riwayat) {
return response()->json([
'success' => false,
'message' => 'Data riwayat tidak ditemukan.',
], 404);
}
$riwayat->delete();
return response()->json([
'success' => true,
'message' => 'Data riwayat berhasil dihapus.',
], 200);
} catch (Throwable $e) {
return response()->json([
'success' => false,
'message' => 'Data riwayat gagal dihapus.',
'error' => $e->getMessage(),
], 500);
}
}
@ -117,15 +160,13 @@ private function getAkurasiPrediksi($riwayat): ?float
{
$tableName = (new RiwayatRekomendasi())->getTable();
if (!Schema::hasColumn($tableName, 'akurasi_prediksi')) {
return null;
foreach (['akurasi_prediksi', 'probabilitas', 'akurasi'] as $column) {
if (Schema::hasColumn($tableName, $column) && $riwayat->{$column} !== null) {
return round((float) $riwayat->{$column}, 2);
}
}
if ($riwayat->akurasi_prediksi === null) {
return null;
}
return round((float) $riwayat->akurasi_prediksi, 2);
return null;
}
private function getRataNilai($riwayat): float
@ -148,7 +189,7 @@ private function getRataNilai($riwayat): float
$riwayat->informatika,
];
$nilai = array_filter($nilai, fn ($item) => $item !== null);
$nilai = array_filter($nilai, fn ($item) => $item !== null && $item !== '');
if (count($nilai) === 0) {
return 0;
@ -175,6 +216,10 @@ private function getMinatUtama($riwayat): string
private function getLabelEkonomi($gaji): string
{
if ($gaji === null || $gaji === '') {
return '-';
}
$gaji = (float) $gaji;
if ($gaji < 2000000) {

View File

@ -4,94 +4,99 @@
use App\Http\Controllers\Controller;
use App\Models\Siswa;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\Rule;
use Throwable;
class SiswaController extends Controller
{
public function index(Request $request)
{
$perPage = (int) $request->input('per_page', 7);
$perPage = in_array($perPage, [5, 7]) ? $perPage : 7;
try {
$perPage = (int) $request->input('per_page', 7);
$perPage = in_array($perPage, [5, 7, 10, 25, 50, 100]) ? $perPage : 7;
$search = trim((string) $request->input('search', ''));
$search = trim((string) $request->input('search', ''));
$query = Siswa::with('user')->orderBy('id', 'desc');
$query = Siswa::with(['user', 'jurusan'])->orderBy('id', 'desc');
if ($search !== '') {
$query->where(function ($q) use ($search) {
$q->where('nama_lengkap', 'like', "%{$search}%")
->orWhere('nisn', 'like', "%{$search}%")
->orWhere('kelas', 'like', "%{$search}%")
->orWhereHas('user', function ($user) use ($search) {
$user->where('username', 'like', "%{$search}%");
});
});
}
if ($search !== '') {
$query->where(function ($q) use ($search) {
$q->where('id_siswa', 'like', "%{$search}%")
->orWhere('nama_lengkap', 'like', "%{$search}%")
->orWhere('nisn', 'like', "%{$search}%")
->orWhere('kelas', 'like', "%{$search}%")
->orWhere('peminatan', 'like', "%{$search}%")
->orWhereHas('user', function ($user) use ($search) {
$user->where('name', 'like', "%{$search}%")
->orWhere('email', 'like', "%{$search}%");
})
->orWhereHas('jurusan', function ($jurusan) use ($search) {
$jurusan->where('nama_jurusan', 'like', "%{$search}%");
});
});
$siswa = $query->paginate($perPage);
return response()->json(['success' => true, 'data' => $siswa], 200);
} catch (Throwable $e) {
return response()->json(['success' => false, 'message' => $e->getMessage()], 500);
}
$siswa = $query->paginate($perPage);
return response()->json([
'success' => true,
'data' => $siswa->items(),
'current_page' => $siswa->currentPage(),
'last_page' => $siswa->lastPage(),
'per_page' => $siswa->perPage(),
'total' => $siswa->total(),
'from' => $siswa->firstItem(),
'to' => $siswa->lastItem(),
], 200);
}
public function store(Request $request)
{
$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',
], [
'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.',
'username' => ['required', Rule::unique(User::class, 'username')],
'password' => 'required|min:6',
'nama_lengkap' => 'required',
'nisn' => ['required', Rule::unique(Siswa::class, 'nisn')],
'kelas' => 'required',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'message' => 'Validasi gagal',
'errors' => $validator->errors()
], 422);
return response()->json(['success' => false, 'message' => 'Validasi gagal', 'errors' => $validator->errors()], 422);
}
$siswa = Siswa::create($request->all());
DB::beginTransaction();
try {
$usernameClean = str_replace(' ', '', $request->username);
$otomatisEmail = strtolower($usernameClean) . '@gmail.com';
return response()->json([
'success' => true,
'message' => 'Siswa ' . $siswa->id_siswa . ' (' . $siswa->nama_lengkap . ') berhasil ditambahkan.',
'data' => $siswa
], 201);
$user = User::create([
'name' => $request->nama_lengkap,
'username' => $request->username,
'email' => $otomatisEmail,
'password' => Hash::make($request->password),
'role' => 'siswa',
]);
Siswa::create([
'user_id' => $user->id,
'nama_lengkap' => $request->nama_lengkap,
'nisn' => $request->nisn,
'kelas' => $request->kelas,
]);
DB::commit();
return response()->json(['success' => true, 'message' => 'Data siswa berhasil ditambahkan.'], 201);
} catch (\Throwable $e) {
DB::rollBack();
return response()->json(['success' => false, 'message' => 'Gagal menyimpan data.', 'error' => $e->getMessage()], 500);
}
}
public function show($id)
{
$siswa = Siswa::find($id);
try {
$siswa = Siswa::with('user')->find($id);
if (!$siswa) {
return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
if (!$siswa) {
return response()->json(['success' => false, 'message' => 'Data siswa tidak ditemukan.'], 404);
}
return response()->json(['success' => true, 'data' => $siswa], 200);
} catch (Throwable $e) {
return response()->json(['success' => false, 'message' => 'Gagal memuat rincian.', 'error' => $e->getMessage()], 500);
}
return response()->json(['success' => true, 'data' => $siswa], 200);
}
public function update(Request $request, $id)
@ -99,39 +104,85 @@ public function update(Request $request, $id)
$siswa = Siswa::find($id);
if (!$siswa) {
return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
return response()->json(['success' => false, 'message' => 'Data siswa 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.'
'username' => [
'nullable',
Rule::unique(User::class, 'username')->ignore($siswa->user_id)
],
'nisn' => [
'required',
Rule::unique(Siswa::class, 'nisn')->ignore($siswa->id)
],
'nama_lengkap' => 'required',
'kelas' => 'required',
]);
if ($validator->fails()) {
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
return response()->json(['success' => false, 'message' => 'Validasi gagal', 'errors' => $validator->errors()], 422);
}
$siswa->update($request->all());
DB::beginTransaction();
try {
if ($siswa->user_id) {
$user = User::find($siswa->user_id);
if ($user) {
$user->name = $request->nama_lengkap;
return response()->json([
'success' => true,
'message' => 'Data ' . $siswa->id_siswa . ' berhasil diperbarui'
], 200);
if ($request->filled('username')) {
$user->username = $request->username;
$usernameClean = str_replace(' ', '', $request->username);
$user->email = strtolower($usernameClean) . '@gmail.com';
}
if ($request->filled('password')) {
$user->password = Hash::make($request->password);
}
$user->save();
}
}
$siswa->update([
'nama_lengkap' => $request->nama_lengkap,
'nisn' => $request->nisn,
'kelas' => $request->kelas
]);
DB::commit();
return response()->json([
'success' => true,
'message' => 'Data siswa berhasil diperbarui.',
'data' => $siswa->load('user')
], 200);
} catch (Throwable $e) {
DB::rollBack();
return response()->json(['success' => false, 'message' => 'Gagal memperbarui data.', 'error' => $e->getMessage()], 500);
}
}
public function destroy($id)
{
$siswa = Siswa::find($id);
try {
$siswa = Siswa::find($id);
if (!$siswa) {
return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
if (!$siswa) {
return response()->json(['success' => false, 'message' => 'Data tidak ditemukan.'], 404);
}
if ($siswa->user_id) {
$userId = $siswa->user_id;
$siswa->delete();
User::destroy($userId);
} else {
$siswa->delete();
}
return response()->json(['success' => true, 'message' => 'Data berhasil dihapus.'], 200);
} catch (Throwable $e) {
return response()->json(['success' => false, 'message' => 'Gagal menghapus.', 'error' => $e->getMessage()], 500);
}
$siswa->delete();
return response()->json(['success' => true, 'message' => 'Data berhasil dihapus'], 200);
}
}

View File

@ -6,7 +6,6 @@
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
class AuthController extends Controller
{
@ -26,10 +25,8 @@ public function login(Request $request)
], 401);
}
// Hapus token lama
$user->tokens()->delete();
// Buat token baru
$token = $user->createToken('auth_token')->plainTextToken;
return response()->json([
@ -37,8 +34,9 @@ public function login(Request $request)
'message' => 'Login berhasil',
'access_token' => $token,
'token_type' => 'Bearer',
'role' => $user->role, // Penting untuk redirect di frontend
'role' => $user->role,
'user' => [
'id' => $user->id,
'name' => $user->name,
'username' => $user->username,
]
@ -47,7 +45,15 @@ public function login(Request $request)
public function logout(Request $request)
{
$request->user()->currentAccessToken()->delete();
return response()->json(['message' => 'Berhasil logout']);
$token = $request->user()?->currentAccessToken();
if ($token) {
$token->delete();
}
return response()->json([
'success' => true,
'message' => 'Berhasil logout'
]);
}
}

View File

@ -14,8 +14,11 @@ public function index()
try {
$user = Auth::user();
if (!$user) {
return response()->json(['success' => false, 'message' => 'Unauthorized'], 401);
if (! $user) {
return response()->json([
'success' => false,
'message' => 'Unauthorized'
], 401);
}
$totalAktivitas = RiwayatRekomendasi::where('user_id', $user->id)->count();
@ -32,7 +35,7 @@ public function index()
return [
'id' => $item->id,
'jurusan' => $item->hasil_rekomendasi_jurusan,
'tanggal' => $item->created_at->diffForHumans(),
'tanggal' => $item->created_at ? $item->created_at->diffForHumans() : '-',
];
});
@ -41,17 +44,30 @@ public function index()
->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'
'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;
$totalNilai += (float) ($item->$m ?? 0);
}
$rataRata = $totalNilai / count($mapel);
$rataRata = count($mapel) > 0 ? $totalNilai / count($mapel) : 0;
return [
'label' => 'Tes ' . ($index + 1),
@ -63,7 +79,9 @@ public function index()
'success' => true,
'data' => [
'total_aktivitas' => $totalAktivitas,
'rekomendasi_terakhir' => $rekomendasiTerakhir ? $rekomendasiTerakhir->hasil_rekomendasi_jurusan : 'Belum Ada Data',
'rekomendasi_terakhir' => $rekomendasiTerakhir
? $rekomendasiTerakhir->hasil_rekomendasi_jurusan
: 'Belum Ada Data',
'aktivitas_terbaru' => $aktivitasTerbaru,
'tren_nilai' => $trenNilai
]

View File

@ -4,18 +4,26 @@
use App\Http\Controllers\Controller;
use App\Models\RiwayatRekomendasi;
use App\Models\Jurusan;
use App\Services\PythonRecommendationService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Schema;
class RecommendationApiController extends Controller
{
public function hitungPrediksi(Request $request)
public function health(PythonRecommendationService $pythonService)
{
$result = $pythonService->health();
return response()->json($result, $result['success'] ? 200 : 500);
}
public function hitungPrediksi(Request $request, PythonRecommendationService $pythonService)
{
$user = Auth::user();
if (!$user) {
if (! $user) {
return response()->json([
'success' => false,
'message' => 'User belum login.'
@ -49,49 +57,52 @@ public function hitungPrediksi(Request $request)
'gaji_ortu' => 'required|numeric|min:0',
]);
$payload = $validated;
$payload['ekonomi_orang_tua'] = self::getLabelEkonomiSingkat($validated['gaji_ortu']);
$payloadPython = $validated;
$payloadPython['ekonomi_orang_tua'] = self::getLabelEkonomiSingkat($validated['gaji_ortu']);
unset($payloadPython['gaji_ortu']);
try {
$response = Http::timeout(15)
->acceptJson()
->post('http://127.0.0.1:5000/predict', $payload);
$pythonResult = $pythonService->predict($payloadPython);
if (!$response->successful()) {
if (! $pythonResult['success']) {
return response()->json([
'success' => false,
'message' => 'ML Engine Error: ' . ($response->json()['message'] ?? $response->body())
'message' => 'ML Engine Error: ' . ($pythonResult['message'] ?? 'Tidak dapat menghubungi Flask API.'),
'detail' => $pythonResult,
], 500);
}
$responseData = $response->json();
$responseData = $pythonResult['data'];
if (!isset($responseData['success']) || !$responseData['success']) {
if (! is_array($responseData)) {
return response()->json([
'success' => false,
'message' => 'Respons Flask API tidak valid.'
], 500);
}
if (($responseData['success'] ?? true) === false) {
return response()->json([
'success' => false,
'message' => 'ML Engine Error: ' . ($responseData['message'] ?? 'Unknown Error')
], 500);
}
$teksJurusan = $responseData['prediksi_jurusan'] ?? null;
$teksJurusan = $responseData['prediksi_jurusan']
?? $responseData['rekomendasi']
?? $responseData['jurusan']
?? null;
if (!$teksJurusan) {
if (! $teksJurusan) {
return response()->json([
'success' => false,
'message' => 'Prediksi jurusan tidak ditemukan pada respons Flask.'
], 500);
}
/*
* Ambil nilai probabilitas dari Flask.
* Contoh respons Flask yang disarankan:
* {
* "success": true,
* "prediksi_jurusan": "Teknik Informatika",
* "probabilitas": 86.45
* }
*/
$akurasiPrediksi = self::normalisasiAkurasi($responseData['probabilitas'] ?? null);
$akurasiPrediksi = self::ambilAkurasiDariResponse($responseData, $teksJurusan);
$dataRiwayat = [
'user_id' => $user->id,
@ -123,16 +134,16 @@ public function hitungPrediksi(Request $request)
'hasil_rekomendasi_jurusan' => $teksJurusan,
];
/*
* Kolom akurasi_prediksi hanya diisi kalau migration baru sudah dijalankan.
* Ini mencegah error kalau kolom belum ada.
*/
$tableName = (new RiwayatRekomendasi())->getTable();
if (Schema::hasColumn($tableName, 'akurasi_prediksi')) {
$dataRiwayat['akurasi_prediksi'] = $akurasiPrediksi;
}
if (Schema::hasColumn($tableName, 'probabilitas')) {
$dataRiwayat['probabilitas'] = $akurasiPrediksi;
}
$riwayat = RiwayatRekomendasi::create($dataRiwayat);
return response()->json([
@ -140,7 +151,9 @@ public function hitungPrediksi(Request $request)
'message' => 'Prediksi berhasil dibuat.',
'redirect_url' => route('student.result', ['id' => $riwayat->id]),
'prediksi_jurusan' => $teksJurusan,
'akurasi_prediksi' => $akurasiPrediksi
'akurasi_prediksi' => $akurasiPrediksi,
'top_predictions' => $responseData['top_predictions'] ?? null,
'riwayat_id' => $riwayat->id,
]);
} catch (\Throwable $e) {
@ -153,7 +166,16 @@ public function hitungPrediksi(Request $request)
public function getHasilRekomendasi($id)
{
$riwayat = RiwayatRekomendasi::findOrFail($id);
$user = Auth::user();
if (! $user) {
return response()->json([
'success' => false,
'message' => 'User belum login.'
], 401);
}
$riwayat = RiwayatRekomendasi::where('user_id', $user->id)->findOrFail($id);
return response()->json([
'success' => true,
@ -166,7 +188,7 @@ public static function formatHasilData(RiwayatRekomendasi $riwayat): array
return [
'id' => $riwayat->id,
'hasil_rekomendasi_jurusan' => $riwayat->hasil_rekomendasi_jurusan,
'akurasi_prediksi' => $riwayat->akurasi_prediksi,
'akurasi_prediksi' => $riwayat->akurasi_prediksi ?? $riwayat->probabilitas ?? null,
'label_ekonomi' => self::getLabelEkonomiDetail($riwayat->gaji_ortu),
'minat_utama' => self::getMinatUtamaLabel($riwayat),
'prospek_kerja' => self::getProspekKerjaText($riwayat->hasil_rekomendasi_jurusan),
@ -182,6 +204,34 @@ public static function formatHasilData(RiwayatRekomendasi $riwayat): array
];
}
private static function ambilAkurasiDariResponse(array $responseData, ?string $jurusan = null): ?float
{
$nilaiLangsung = $responseData['probabilitas']
?? $responseData['confidence']
?? $responseData['akurasi_prediksi']
?? null;
if ($nilaiLangsung !== null) {
return self::normalisasiAkurasi($nilaiLangsung);
}
$topPredictions = $responseData['top_predictions'] ?? null;
if (! is_array($topPredictions) || count($topPredictions) === 0) {
return null;
}
if ($jurusan) {
foreach ($topPredictions as $item) {
if (($item['jurusan'] ?? null) === $jurusan) {
return self::normalisasiAkurasi($item['probabilitas'] ?? null);
}
}
}
return self::normalisasiAkurasi($topPredictions[0]['probabilitas'] ?? null);
}
private static function normalisasiAkurasi($value): ?float
{
if ($value === null || $value === '') {
@ -194,28 +244,16 @@ private static function normalisasiAkurasi($value): ?float
$value = trim($value);
}
if (!is_numeric($value)) {
if (! is_numeric($value)) {
return null;
}
$number = (float) $value;
/*
* Jika Flask mengirim 0.86, ubah menjadi 86.
* Jika Flask mengirim 86.45, tetap 86.45.
*/
if ($number <= 1) {
if ($number >= 0 && $number <= 1) {
$number = $number * 100;
}
if ($number < 0) {
$number = 0;
}
if ($number > 100) {
$number = 100;
}
return round($number, 2);
}
@ -225,7 +263,9 @@ public static function getLabelEkonomiSingkat($gaji): string
if ($gaji < 2000000) {
return 'Kurang Mampu';
} elseif ($gaji <= 5000000) {
}
if ($gaji <= 5000000) {
return 'Menengah';
}
@ -237,60 +277,65 @@ public static function getLabelEkonomiDetail($gaji): string
$gaji = (float) $gaji;
if ($gaji < 2000000) {
return 'Kurang Mampu (< Rp 2.000.000)';
} elseif ($gaji <= 5000000) {
return 'Menengah (Rp 2.000.000 - Rp 5.000.000)';
return 'Ekonomi Kurang Mampu';
}
return 'Mampu (> Rp 5.000.000)';
if ($gaji <= 5000000) {
return 'Ekonomi Menengah';
}
return 'Ekonomi Mampu';
}
public static function getMinatUtamaLabel(RiwayatRekomendasi $riwayat): string
{
$riasecScores = [
'r' => (float) $riwayat->riasec_r,
'i' => (float) $riwayat->riasec_i,
'a' => (float) $riwayat->riasec_a,
's' => (float) $riwayat->riasec_s,
'e' => (float) $riwayat->riasec_e,
'c' => (float) $riwayat->riasec_c,
$scores = [
'Realistic' => (float) $riwayat->riasec_r,
'Investigative' => (float) $riwayat->riasec_i,
'Artistic' => (float) $riwayat->riasec_a,
'Social' => (float) $riwayat->riasec_s,
'Enterprising' => (float) $riwayat->riasec_e,
'Conventional' => (float) $riwayat->riasec_c,
];
arsort($riasecScores);
$topCode = array_key_first($riasecScores);
arsort($scores);
$labels = [
'r' => 'Teknis & Lapangan',
'i' => 'Sains, Riset & Analitis',
'a' => 'Kreatif & Desain',
's' => 'Sosial & Pelayanan',
'e' => 'Bisnis & Kepemimpinan',
'c' => 'Administratif & Terstruktur',
];
return $labels[$topCode] ?? 'Belum teridentifikasi';
return array_key_first($scores) ?? 'Belum diketahui';
}
public static function getProspekKerjaText($jurusan): string
{
$jurusan = strtolower((string) $jurusan);
if (str_contains($jurusan, 'ekonomi') || str_contains($jurusan, 'manajemen') || str_contains($jurusan, 'bisnis')) {
return 'Lulusan bidang ini dapat berkarier sebagai manajer bisnis, staf keuangan, analis pasar, konsultan bisnis, wirausahawan, maupun pegawai instansi pemerintah dan BUMN pada sektor ekonomi dan administrasi.';
}
if (str_contains($jurusan, 'teknik') || str_contains($jurusan, 'informatika') || str_contains($jurusan, 'komputer')) {
return 'Lulusan bidang ini memiliki peluang karier sebagai software developer, analis sistem, teknisi, network engineer, data analyst, maupun profesional teknologi di perusahaan swasta, startup, dan instansi pemerintah.';
}
if (str_contains($jurusan, 'kesehatan') || str_contains($jurusan, 'keperawatan') || str_contains($jurusan, 'farmasi')) {
return 'Lulusan bidang ini dapat berkarier di rumah sakit, klinik, laboratorium, industri kesehatan, layanan masyarakat, maupun lembaga penelitian kesehatan.';
}
if (str_contains($jurusan, 'pendidikan') || str_contains($jurusan, 'guru')) {
return 'Lulusan bidang ini memiliki prospek sebagai guru, tenaga pendidik, instruktur pelatihan, pengembang kurikulum, maupun konsultan pendidikan.';
}
return 'Prospek karier untuk jurusan ini sangat luas, mulai dari spesialis industri, konsultan ahli, hingga pegawai instansi pemerintah/BUMN di sektor terkait.';
public static function getProspekKerjaText(?string $jurusan): string
{
if (! $jurusan) {
return 'Prospek kerja belum tersedia.';
}
// 1. Cari data Jurusan di database berdasarkan nama jurusan hasil prediksi
$dataJurusan = \App\Models\Jurusan::where('nama_jurusan', $jurusan)->first();
// 2. Jika jurusan tidak ditemukan atau tidak memiliki data prospek kerja
if (! $dataJurusan || $dataJurusan->prospeks->isEmpty()) {
return 'Prospek kerja untuk program studi ' . $jurusan . ' belum diinputkan oleh admin.';
}
// 3. Ambil semua data prospek kerja terkait dan susun menjadi format HTML list yang rapi
$htmlOutput = '<ul class="space-y-3">';
foreach ($dataJurusan->prospeks as $prospek) {
$htmlOutput .= '<li class="border-b border-slate-100 pb-2 last:border-0 last:pb-0">';
$htmlOutput .= ' <strong class="text-slate-800 block text-base">' . e($prospek->nama_profesi) . '</strong>';
// Tampilkan range gaji jika admin mengisinya
if (!empty($prospek->range_gaji)) {
$htmlOutput .= ' <span class="inline-block bg-amber-50 text-amber-700 text-xs px-2 py-0.5 rounded-md font-medium mt-1 mb-1">💰 Estimasi: ' . e($prospek->range_gaji) . '</span>';
}
$htmlOutput .= ' <p class="text-slate-500 text-sm mt-0.5 leading-relaxed">' . e($prospek->deskripsi_pekerjaan) . '</p>';
$htmlOutput .= '</li>';
}
$htmlOutput .= '</ul>';
return $htmlOutput;
}
}

View File

@ -19,13 +19,18 @@ protected static function boot()
{
parent::boot();
// Otomatis membuat id_alumni yang unik dengan aman saat data dibuat
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);
if (empty($model->id_alumni)) {
$nextNumber = ((int) static::max('id')) + 1;
$candidate = 'ALM' . str_pad($nextNumber, 4, '0', STR_PAD_LEFT);
while (static::where('id_alumni', $candidate)->exists()) {
$nextNumber++;
$candidate = 'ALM' . str_pad($nextNumber, 4, '0', STR_PAD_LEFT);
}
$model->id_alumni = $candidate;
}
});
}

View File

@ -3,22 +3,37 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Guru extends Model
{
protected $fillable = ['id_guru', 'nama_guru'];
// Memastikan nama tabel sesuai dengan database
protected $table = 'gurus';
protected $fillable = ['user_id', 'id_guru', 'nama_guru'];
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}
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);
if (empty($model->id_guru)) {
$nextNumber = ((int) static::max('id')) + 1;
$candidate = 'GRU' . str_pad($nextNumber, 4, '0', STR_PAD_LEFT);
while (static::where('id_guru', $candidate)->exists()) {
$nextNumber++;
$candidate = 'GRU' . str_pad($nextNumber, 4, '0', STR_PAD_LEFT);
}
$model->id_guru = $candidate;
}
});
}

View File

@ -6,7 +6,7 @@
class Jurusan extends Model
{
protected $fillable = ['nama_jurusan', 'rumpun', 'deskripsi'];
protected $fillable = ['nama_jurusan', 'rumpun',];
// Relasi: Satu Jurusan memiliki banyak Prospek Kerja
public function prospeks()

View File

@ -5,17 +5,15 @@
use Illuminate\Database\Eloquent\Model;
class Siswa extends Model
{
protected $table = 'siswas';
protected $fillable = [
'id_siswa',
'user_id',
'jurusan_id',
'nama_lengkap',
'nisn',
'kelas',
'peminatan',
'minat_bakat',
'ekonomi'
'kelas'
];
protected static function boot()
@ -39,8 +37,4 @@ protected static function boot()
public function user() {
return $this->belongsTo(User::class);
}
public function jurusan() {
return $this->belongsTo(Jurusan::class);
}
}

View File

@ -11,7 +11,6 @@
class User extends Authenticatable
{
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory, Notifiable;
use HasApiTokens, HasFactory, Notifiable;
/**

View File

@ -0,0 +1,89 @@
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Throwable;
class PythonRecommendationService
{
protected string $baseUrl;
protected int $timeout;
public function __construct()
{
$this->baseUrl = rtrim((string) config('services.python_api.url'), '/');
$this->timeout = (int) config('services.python_api.timeout', 30);
}
public function health(): array
{
try {
if ($this->baseUrl === '') {
return [
'success' => false,
'status_code' => 500,
'data' => null,
'message' => 'PYTHON_API_URL belum diatur pada file .env.',
];
}
$response = Http::timeout($this->timeout)
->acceptJson()
->get($this->baseUrl . '/health');
return [
'success' => $response->successful(),
'status_code' => $response->status(),
'data' => $response->json(),
'message' => $response->successful()
? 'Laravel berhasil terhubung ke Flask API.'
: 'Laravel gagal membaca Flask API.',
];
} catch (Throwable $e) {
return [
'success' => false,
'status_code' => 500,
'data' => null,
'message' => 'Tidak dapat terhubung ke Flask API: ' . $e->getMessage(),
];
}
}
public function predict(array $payload): array
{
try {
if ($this->baseUrl === '') {
return [
'success' => false,
'status_code' => 500,
'data' => null,
'message' => 'PYTHON_API_URL belum diatur pada file .env.',
];
}
$response = Http::timeout($this->timeout)
->acceptJson()
->asJson()
->post($this->baseUrl . '/predict', $payload);
$json = $response->json();
return [
'success' => $response->successful(),
'status_code' => $response->status(),
'data' => $json,
'message' => $response->successful()
? 'Prediksi berhasil diproses oleh Flask API.'
: ($json['message'] ?? $response->body() ?? 'Prediksi gagal diproses oleh Flask API.'),
];
} catch (Throwable $e) {
return [
'success' => false,
'status_code' => 500,
'data' => null,
'message' => 'Koneksi ke Flask API gagal: ' . $e->getMessage(),
];
}
}
}

View File

@ -6,12 +6,6 @@
|--------------------------------------------------------------------------
| Third Party Services
|--------------------------------------------------------------------------
|
| This file is for storing the credentials for third party services such
| as Mailgun, Postmark, AWS and more. This file provides the de facto
| location for this type of information, allowing packages to have
| a conventional file to locate the various service credentials.
|
*/
'postmark' => [
@ -35,4 +29,15 @@
],
],
/*
|--------------------------------------------------------------------------
| Python Flask API
|--------------------------------------------------------------------------
*/
'python_api' => [
'url' => env('PYTHON_API_URL', 'https://galangadji.pythonanywhere.com'),
'timeout' => env('PYTHON_API_TIMEOUT', 30),
],
];

View File

@ -2,6 +2,6 @@ flask
flask-cors
pandas
numpy
scikit-learn==1.6.1
scikit-learn==1.8.0
joblib
openpyxl

18
index.php Normal file
View File

@ -0,0 +1,18 @@
<?php
use Illuminate\Foundation\Application;
use Illuminate\Http\Request;
define('LARAVEL_START', microtime(true));
if (file_exists($maintenance = __DIR__.'/storage/framework/maintenance.php')) {
require $maintenance;
}
require __DIR__.'/vendor/autoload.php';
$app = require_once __DIR__.'/bootstrap/app.php';
$app->usePublicPath(__DIR__);
$app->handleRequest(Request::capture());

View File

@ -45,6 +45,7 @@
<th class="text-center" width="150">Opsi</th>
</tr>
</thead>
<tbody id="alumni-table-body">
<tr>
<td colspan="6" class="loading-state-modern">
@ -65,13 +66,39 @@
</div>
<script>
function getAppBaseUrl() {
const origin = window.location.origin;
const segments = window.location.pathname.split('/').filter(Boolean);
const adminIndex = segments.indexOf('admin');
if (adminIndex > 0) {
return origin + '/' + segments.slice(0, adminIndex).join('/');
}
return origin;
}
const APP_BASE_URL = getAppBaseUrl();
const API_ALUMNI_URL = `${APP_BASE_URL}/api/admin/alumni`;
const LOGIN_URL = `${APP_BASE_URL}/login`;
const ALUMNI_EDIT_BASE_URL = `${APP_BASE_URL}/admin/alumni`;
let currentPage = 1;
let currentSearch = '';
let perPage = 7;
let searchTimer = null;
function getToken() {
return localStorage.getItem('access_token') ||
localStorage.getItem('token') ||
sessionStorage.getItem('access_token') ||
sessionStorage.getItem('token') ||
'';
}
function escapeHtml(value) {
if (value === null || value === undefined || value === '') return '-';
return String(value)
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
@ -85,6 +112,19 @@ function getInitial(name) {
return cleanName.charAt(0).toUpperCase();
}
async function parseJsonResponse(response) {
const text = await response.text();
try {
return text ? JSON.parse(text) : {};
} catch (error) {
return {
success: false,
message: text || response.statusText || 'Respon server tidak valid.'
};
}
}
function showLoading() {
document.getElementById('alumni-table-body').innerHTML = `
<tr>
@ -96,11 +136,11 @@ function showLoading() {
}
async function fetchAlumni(page = 1) {
const token = localStorage.getItem('access_token');
const token = getToken();
const tbody = document.getElementById('alumni-table-body');
if (!token) {
window.location.href = '/login';
window.location.href = LOGIN_URL;
return;
}
@ -117,14 +157,15 @@ function showLoading() {
}
try {
const response = await fetch(`/api/admin/alumni?${params.toString()}`, {
const response = await fetch(`${API_ALUMNI_URL}?${params.toString()}`, {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + token,
'Accept': 'application/json'
}
});
const result = await response.json();
const result = await parseJsonResponse(response);
if (!response.ok || result.success === false) {
throw new Error(result.message || 'Gagal memuat data alumni.');
@ -142,16 +183,21 @@ function showLoading() {
</td>
</tr>
`;
updatePaginationInfo(null);
renderPagination({ current_page: 1, last_page: 1, total: 0 });
return;
}
data.forEach((item, index) => {
const editUrl = `${ALUMNI_EDIT_BASE_URL}/${encodeURIComponent(item.id)}/edit`;
tbody.innerHTML += `
<tr>
<td class="text-center text-muted fw-bold">${(result.from || 1) + index}</td>
<td class="text-center fw-bold text-muted">${escapeHtml(item.id_alumni)}</td>
<td>
<div class="table-user-cell">
<div class="avatar-circle blue">${getInitial(item.nama_alumni)}</div>
@ -161,16 +207,20 @@ function showLoading() {
</div>
</div>
</td>
<td class="text-cell-muted">${escapeHtml(item.perguruan_tinggi)}</td>
<td class="text-muted fw-semibold">${escapeHtml(item.perguruan_tinggi)}</td>
<td class="text-center">
<span class="badge-soft badge-result">${escapeHtml(item.jurusan_diambil)}</span>
<span class="badge-soft badge-kelas">${escapeHtml(item.jurusan_diambil)}</span>
</td>
<td class="text-center">
<div class="opsi-wrapper">
<a href="/admin/alumni/${item.id}/edit" class="btn-action btn-edit" title="Edit">
<a href="${editUrl}" 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">
<button type="button" onclick="deleteAlumni(${item.id})" class="btn-action btn-delete" title="Hapus">
<i class="fas fa-trash"></i>
</button>
</div>
@ -187,7 +237,7 @@ function showLoading() {
<tr>
<td colspan="6" class="empty-state-modern text-danger">
<i class="fas fa-exclamation-circle"></i>
Gagal memuat data alumni.
${escapeHtml(error.message || 'Gagal memuat data alumni.')}
</td>
</tr>
`;
@ -220,24 +270,32 @@ function renderPagination(data) {
btn.className = `page-link-custom ${active ? 'active' : ''}`;
btn.innerHTML = label;
btn.disabled = disabled;
btn.onclick = () => {
if (!disabled && !active) fetchAlumni(page);
};
return btn;
};
controls.appendChild(createButton('<i class="fas fa-chevron-left"></i>', current - 1, current === 1));
let pages = [];
if (last <= 5) {
for (let i = 1; i <= last; i++) pages.push(i);
} else {
pages.push(1);
if (current > 3) pages.push('...');
const start = Math.max(2, current - 1);
const end = Math.min(last - 1, current + 1);
for (let i = start; i <= end; i++) pages.push(i);
if (current < last - 2) pages.push('...');
pages.push(last);
}
@ -253,9 +311,9 @@ function renderPagination(data) {
}
async function deleteAlumni(id) {
const result = await Swal.fire({
title: 'Hapus Data?',
text: 'Data alumni akan dihapus secara permanen!',
const confirmResult = await Swal.fire({
title: 'Hapus data?',
text: 'Data alumni akan dihapus permanen!',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#ef4444',
@ -264,12 +322,12 @@ function renderPagination(data) {
cancelButtonText: 'Batal'
});
if (!result.isConfirmed) return;
if (!confirmResult.isConfirmed) return;
const token = localStorage.getItem('access_token');
const token = getToken();
try {
const response = await fetch('/api/admin/alumni/' + id, {
const response = await fetch(`${API_ALUMNI_URL}/${encodeURIComponent(id)}`, {
method: 'DELETE',
headers: {
'Authorization': 'Bearer ' + token,
@ -277,14 +335,16 @@ function renderPagination(data) {
}
});
if (response.ok) {
Swal.fire('Terhapus!', 'Data alumni berhasil dihapus.', 'success');
const result = await parseJsonResponse(response);
if (response.ok && result.success !== false) {
Swal.fire('Berhasil!', result.message || 'Data alumni berhasil dihapus.', 'success');
fetchAlumni(currentPage);
} else {
Swal.fire('Gagal!', 'Data alumni gagal dihapus.', 'error');
Swal.fire('Gagal!', result.message || 'Data alumni gagal dihapus.', 'error');
}
} catch (error) {
Swal.fire('Error!', 'Koneksi ke server gagal.', 'error');
Swal.fire('Error!', error.message || 'Koneksi ke server gagal.', 'error');
}
}
@ -293,6 +353,7 @@ function renderPagination(data) {
document.getElementById('searchInput').addEventListener('input', function () {
clearTimeout(searchTimer);
searchTimer = setTimeout(() => {
currentSearch = this.value.trim();
fetchAlumni(1);

View File

@ -19,18 +19,26 @@
<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>
<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>
@ -39,34 +47,120 @@
</div>
<script>
document.getElementById('formCreateAlumni').addEventListener('submit', async (e) => {
function getAppBaseUrl() {
const origin = window.location.origin;
const segments = window.location.pathname.split('/').filter(Boolean);
const adminIndex = segments.indexOf('admin');
if (adminIndex > 0) {
return origin + '/' + segments.slice(0, adminIndex).join('/');
}
return origin;
}
const APP_BASE_URL = getAppBaseUrl();
const API_ALUMNI_URL = `${APP_BASE_URL}/api/admin/alumni`;
const LOGIN_URL = `${APP_BASE_URL}/login`;
const ALUMNI_INDEX_URL = `${APP_BASE_URL}/admin/alumni`;
function getToken() {
return localStorage.getItem('access_token') ||
localStorage.getItem('token') ||
sessionStorage.getItem('access_token') ||
sessionStorage.getItem('token') ||
'';
}
function getValue(id) {
return document.getElementById(id).value.trim();
}
async function parseJsonResponse(response) {
const text = await response.text();
try {
return text ? JSON.parse(text) : {};
} catch (error) {
return {
success: false,
message: text || response.statusText || 'Respon server tidak valid.'
};
}
}
function formatErrors(errors) {
if (!errors) return '';
return Object.values(errors)
.flat()
.map(item => String(item))
.join('<br>');
}
document.getElementById('formCreateAlumni').addEventListener('submit', async function (e) {
e.preventDefault();
const token = getToken();
if (!token) {
Swal.fire('Sesi Berakhir', 'Silakan login ulang.', 'warning')
.then(() => window.location.href = LOGIN_URL);
return;
}
const btn = document.getElementById('btnSave');
btn.disabled = true;
btn.innerText = 'Menyimpan...';
const payload = {
nama_alumni: document.getElementById('nama_alumni').value,
perguruan_tinggi: document.getElementById('perguruan_tinggi').value,
jurusan_diambil: document.getElementById('jurusan_diambil').value
nama_alumni: getValue('nama_alumni'),
perguruan_tinggi: getValue('perguruan_tinggi'),
jurusan_diambil: getValue('jurusan_diambil')
};
try {
const response = await fetch('/api/admin/alumni', {
const response = await fetch(API_ALUMNI_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('access_token'),
'Authorization': 'Bearer ' + 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') }}"; });
const result = await parseJsonResponse(response);
if (response.status === 401 || response.status === 403) {
Swal.fire('Sesi Berakhir', 'Silakan login ulang.', 'warning')
.then(() => window.location.href = LOGIN_URL);
return;
}
} catch (error) { Swal.fire('Error!', 'Gagal menghubungi server.', 'error'); }
finally { btn.disabled = false; }
if (response.ok && result.success !== false) {
Swal.fire({
icon: 'success',
title: 'Berhasil!',
text: result.message || 'Data alumni berhasil ditambahkan.',
confirmButtonColor: '#2D7A47'
}).then(() => {
window.location.href = ALUMNI_INDEX_URL;
});
} else {
Swal.fire({
icon: 'warning',
title: 'Gagal!',
html: formatErrors(result.errors) || result.message || 'Cek kembali data yang dimasukkan.',
confirmButtonColor: '#2D7A47'
});
}
} catch (error) {
Swal.fire('Error!', error.message || 'Gagal menghubungi server.', 'error');
} finally {
btn.disabled = false;
btn.innerText = 'Simpan Alumni';
}
});
</script>
@endsection

View File

@ -15,23 +15,32 @@
<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>
<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>
@ -40,51 +49,159 @@
</div>
<script>
const dbId = document.getElementById('alumni_id_db').value;
function getAppBaseUrl() {
const origin = window.location.origin;
const segments = window.location.pathname.split('/').filter(Boolean);
const adminIndex = segments.indexOf('admin');
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); }
if (adminIndex > 0) {
return origin + '/' + segments.slice(0, adminIndex).join('/');
}
return origin;
}
document.getElementById('formEditAlumni').addEventListener('submit', async (e) => {
const APP_BASE_URL = getAppBaseUrl();
const API_ALUMNI_URL = `${APP_BASE_URL}/api/admin/alumni`;
const LOGIN_URL = `${APP_BASE_URL}/login`;
const ALUMNI_INDEX_URL = `${APP_BASE_URL}/admin/alumni`;
const dbId = document.getElementById('alumni_id_db').value;
function getToken() {
return localStorage.getItem('access_token') ||
localStorage.getItem('token') ||
sessionStorage.getItem('access_token') ||
sessionStorage.getItem('token') ||
'';
}
function getValue(id) {
return document.getElementById(id).value.trim();
}
async function parseJsonResponse(response) {
const text = await response.text();
try {
return text ? JSON.parse(text) : {};
} catch (error) {
return {
success: false,
message: text || response.statusText || 'Respon server tidak valid.'
};
}
}
function formatErrors(errors) {
if (!errors) return '';
return Object.values(errors)
.flat()
.map(item => String(item))
.join('<br>');
}
async function loadAlumni() {
const token = getToken();
if (!token) {
window.location.href = LOGIN_URL;
return;
}
try {
const response = await fetch(`${API_ALUMNI_URL}/${encodeURIComponent(dbId)}`, {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + token,
'Accept': 'application/json'
}
});
const result = await parseJsonResponse(response);
if (response.status === 401 || response.status === 403) {
Swal.fire('Sesi Berakhir', 'Silakan login ulang.', 'warning')
.then(() => window.location.href = LOGIN_URL);
return;
}
if (response.ok && result.success !== false && result.data) {
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 = `EDITING: ${result.data.id_alumni || '-'}`;
} else {
Swal.fire('Gagal!', result.message || 'Data alumni tidak ditemukan.', 'warning');
}
} catch (error) {
Swal.fire('Error!', error.message || 'Gagal mengambil data dari server.', 'error');
}
}
document.getElementById('formEditAlumni').addEventListener('submit', async function (e) {
e.preventDefault();
const token = getToken();
if (!token) {
Swal.fire('Sesi Berakhir', 'Silakan login ulang.', 'warning')
.then(() => window.location.href = LOGIN_URL);
return;
}
const btn = document.getElementById('btnUpdate');
btn.disabled = true;
btn.innerText = 'Memperbarui...';
const payload = {
nama_alumni: document.getElementById('nama_alumni').value,
perguruan_tinggi: document.getElementById('perguruan_tinggi').value,
jurusan_diambil: document.getElementById('jurusan_diambil').value
nama_alumni: getValue('nama_alumni'),
perguruan_tinggi: getValue('perguruan_tinggi'),
jurusan_diambil: getValue('jurusan_diambil')
};
try {
const response = await fetch(`/api/admin/alumni/${dbId}`, {
const response = await fetch(`${API_ALUMNI_URL}/${encodeURIComponent(dbId)}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('access_token'),
'Authorization': 'Bearer ' + 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') }}"; });
const result = await parseJsonResponse(response);
if (response.status === 401 || response.status === 403) {
Swal.fire('Sesi Berakhir', 'Silakan login ulang.', 'warning')
.then(() => window.location.href = LOGIN_URL);
return;
}
} catch (error) { Swal.fire('Error!', 'Gagal update.', 'error'); }
finally { btn.disabled = false; }
if (response.ok && result.success !== false) {
Swal.fire({
icon: 'success',
title: 'Berhasil Diperbarui!',
text: result.message || 'Data alumni berhasil diperbarui.',
confirmButtonColor: '#2D7A47'
}).then(() => {
window.location.href = ALUMNI_INDEX_URL;
});
} else {
Swal.fire({
icon: 'warning',
title: 'Gagal!',
html: formatErrors(result.errors) || result.message || 'Cek kembali data yang dimasukkan.',
confirmButtonColor: '#2D7A47'
});
}
} catch (error) {
Swal.fire('Error!', error.message || 'Kesalahan koneksi server.', 'error');
} finally {
btn.disabled = false;
btn.innerText = 'Perbarui Data';
}
});
document.addEventListener('DOMContentLoaded', loadAlumni);

View File

@ -100,11 +100,47 @@
</section>
</div>
@push('scripts')
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
function getAppBaseUrl() {
const origin = window.location.origin;
const segments = window.location.pathname.split('/').filter(Boolean);
const adminIndex = segments.indexOf('admin');
if (adminIndex > 0) {
return origin + '/' + segments.slice(0, adminIndex).join('/');
}
return origin;
}
const APP_BASE_URL = getAppBaseUrl();
const API_DASHBOARD_URL = `${APP_BASE_URL}/api/admin/dashboard-stats`;
const LOGIN_URL = `${APP_BASE_URL}/login`;
let distribusiChartInstance = null;
function getToken() {
return localStorage.getItem('access_token') ||
localStorage.getItem('token') ||
sessionStorage.getItem('access_token') ||
sessionStorage.getItem('token') ||
'';
}
async function parseJsonResponse(response) {
const text = await response.text();
try {
return text ? JSON.parse(text) : {};
} catch (error) {
return {
success: false,
message: text || response.statusText || 'Respon server tidak valid.'
};
}
}
function setText(id, value) {
const element = document.getElementById(id);
@ -164,9 +200,7 @@ function renderDistribusiChart(distribusi) {
const placeholder = document.getElementById('chart-placeholder');
const canvas = document.getElementById('distribusiChart');
if (!placeholder || !canvas) {
return;
}
if (!placeholder || !canvas) return;
if (!distribusi || distribusi.length === 0) {
placeholder.style.display = 'flex';
@ -270,26 +304,32 @@ function renderDistribusiChart(distribusi) {
}
async function loadDashboardStats() {
const token = localStorage.getItem('access_token');
const token = getToken();
if (!token) {
window.location.href = '/login';
window.location.href = LOGIN_URL;
return;
}
updateLastUpdated();
try {
const response = await fetch('/api/admin/dashboard-stats', {
const response = await fetch(API_DASHBOARD_URL, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/json'
}
});
const result = await response.json();
const result = await parseJsonResponse(response);
if (!response.ok || !result.success) {
if (response.status === 401 || response.status === 403) {
window.location.href = LOGIN_URL;
return;
}
if (!response.ok || result.success === false) {
throw new Error(result.message || 'Gagal memuat data dashboard.');
}
@ -306,7 +346,7 @@ function renderDistribusiChart(distribusi) {
} catch (error) {
console.error('Gagal memuat data dashboard:', error);
setText('akurasi-status', 'Gagal memuat data dashboard.');
setText('akurasi-status', error.message || 'Gagal memuat data dashboard.');
updateAccuracyRing(0);
const placeholder = document.getElementById('chart-placeholder');
@ -325,5 +365,4 @@ function renderDistribusiChart(distribusi) {
document.addEventListener('DOMContentLoaded', loadDashboardStats);
</script>
@endpush
@endsection

View File

@ -43,6 +43,7 @@
<th class="text-center" width="150">Opsi</th>
</tr>
</thead>
<tbody id="guru-table-body">
<tr>
<td colspan="4" class="loading-state-modern">
@ -63,13 +64,39 @@
</div>
<script>
function getAppBaseUrl() {
const origin = window.location.origin;
const segments = window.location.pathname.split('/').filter(Boolean);
const adminIndex = segments.indexOf('admin');
if (adminIndex > 0) {
return origin + '/' + segments.slice(0, adminIndex).join('/');
}
return origin;
}
const APP_BASE_URL = getAppBaseUrl();
const API_GURU_URL = `${APP_BASE_URL}/api/admin/guru`;
const LOGIN_URL = `${APP_BASE_URL}/login`;
const GURU_EDIT_BASE_URL = `${APP_BASE_URL}/admin/guru`;
let currentPage = 1;
let currentSearch = '';
let perPage = 7;
let searchTimer = null;
function getToken() {
return localStorage.getItem('access_token') ||
localStorage.getItem('token') ||
sessionStorage.getItem('access_token') ||
sessionStorage.getItem('token') ||
'';
}
function escapeHtml(value) {
if (value === null || value === undefined || value === '') return '-';
return String(value)
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
@ -83,6 +110,19 @@ function getInitial(name) {
return cleanName.charAt(0).toUpperCase();
}
async function parseJsonResponse(response) {
const text = await response.text();
try {
return text ? JSON.parse(text) : {};
} catch (error) {
return {
success: false,
message: text || response.statusText || 'Respon server tidak valid.'
};
}
}
function showLoading() {
document.getElementById('guru-table-body').innerHTML = `
<tr>
@ -94,11 +134,11 @@ function showLoading() {
}
async function fetchGuru(page = 1) {
const token = localStorage.getItem('access_token');
const token = getToken();
const tbody = document.getElementById('guru-table-body');
if (!token) {
window.location.href = '/login';
window.location.href = LOGIN_URL;
return;
}
@ -115,14 +155,15 @@ function showLoading() {
}
try {
const response = await fetch(`/api/admin/guru?${params.toString()}`, {
const response = await fetch(`${API_GURU_URL}?${params.toString()}`, {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + token,
'Accept': 'application/json'
}
});
const result = await response.json();
const result = await parseJsonResponse(response);
if (!response.ok || result.success === false) {
throw new Error(result.message || 'Gagal memuat data guru.');
@ -140,31 +181,38 @@ function showLoading() {
</td>
</tr>
`;
updatePaginationInfo(null);
renderPagination({ current_page: 1, last_page: 1, total: 0 });
return;
}
data.forEach((item, index) => {
const editUrl = `${GURU_EDIT_BASE_URL}/${encodeURIComponent(item.id)}/edit`;
tbody.innerHTML += `
<tr>
<td class="text-center text-muted fw-bold">${(result.from || 1) + index}</td>
<td class="text-center fw-bold text-muted">${escapeHtml(item.id_guru)}</td>
<td>
<div class="table-user-cell">
<div class="avatar-circle orange">${getInitial(item.nama_guru)}</div>
<div>
<div class="fw-bold text-dark">${escapeHtml(item.nama_guru)}</div>
<div class="text-cell-muted">Guru Bimbingan Konseling</div>
<div class="text-cell-muted">Guru BK</div>
</div>
</div>
</td>
<td class="text-center">
<div class="opsi-wrapper">
<a href="/admin/guru/${item.id}/edit" class="btn-action btn-edit" title="Edit">
<a href="${editUrl}" 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">
<button type="button" onclick="deleteGuru(${item.id})" class="btn-action btn-delete" title="Hapus">
<i class="fas fa-trash"></i>
</button>
</div>
@ -181,7 +229,7 @@ function showLoading() {
<tr>
<td colspan="4" class="empty-state-modern text-danger">
<i class="fas fa-exclamation-circle"></i>
Gagal memuat data guru.
${escapeHtml(error.message || 'Gagal memuat data guru.')}
</td>
</tr>
`;
@ -214,24 +262,32 @@ function renderPagination(data) {
btn.className = `page-link-custom ${active ? 'active' : ''}`;
btn.innerHTML = label;
btn.disabled = disabled;
btn.onclick = () => {
if (!disabled && !active) fetchGuru(page);
};
return btn;
};
controls.appendChild(createButton('<i class="fas fa-chevron-left"></i>', current - 1, current === 1));
let pages = [];
if (last <= 5) {
for (let i = 1; i <= last; i++) pages.push(i);
} else {
pages.push(1);
if (current > 3) pages.push('...');
const start = Math.max(2, current - 1);
const end = Math.min(last - 1, current + 1);
for (let i = start; i <= end; i++) pages.push(i);
if (current < last - 2) pages.push('...');
pages.push(last);
}
@ -247,8 +303,8 @@ function renderPagination(data) {
}
async function deleteGuru(id) {
const result = await Swal.fire({
title: 'Hapus Guru?',
const confirmResult = await Swal.fire({
title: 'Hapus data?',
text: 'Data guru akan dihapus permanen!',
icon: 'warning',
showCancelButton: true,
@ -258,12 +314,12 @@ function renderPagination(data) {
cancelButtonText: 'Batal'
});
if (!result.isConfirmed) return;
if (!confirmResult.isConfirmed) return;
const token = localStorage.getItem('access_token');
const token = getToken();
try {
const response = await fetch('/api/admin/guru/' + id, {
const response = await fetch(`${API_GURU_URL}/${encodeURIComponent(id)}`, {
method: 'DELETE',
headers: {
'Authorization': 'Bearer ' + token,
@ -271,14 +327,16 @@ function renderPagination(data) {
}
});
if (response.ok) {
Swal.fire('Terhapus!', 'Data guru berhasil dihapus.', 'success');
const result = await parseJsonResponse(response);
if (response.ok && result.success !== false) {
Swal.fire('Berhasil!', result.message || 'Data guru berhasil dihapus.', 'success');
fetchGuru(currentPage);
} else {
Swal.fire('Gagal!', 'Data guru gagal dihapus.', 'error');
Swal.fire('Gagal!', result.message || 'Data guru gagal dihapus.', 'error');
}
} catch (error) {
Swal.fire('Error!', 'Koneksi ke server gagal.', 'error');
Swal.fire('Error!', error.message || 'Koneksi ke server gagal.', 'error');
}
}
@ -287,6 +345,7 @@ function renderPagination(data) {
document.getElementById('searchInput').addEventListener('input', function () {
clearTimeout(searchTimer);
searchTimer = setTimeout(() => {
currentSearch = this.value.trim();
fetchGuru(1);

View File

@ -7,22 +7,51 @@
<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>
<h4 class="fw-bold text-dark">Tambah Guru BK Baru</h4>
<p class="text-muted small">Daftarkan akun guru pembimbing baru beserta akun login sistem.</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">
<h6 class="fw-bold text-success border-bottom pb-2 mb-3"><i class="fas fa-user-tie me-2"></i>Data Profil Guru</h6>
</div>
<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>
<input type="text" id="nama_guru" class="form-control input-custom" placeholder="Contoh: Dra. Endang Sri, M.Pd" required>
</div>
<div class="col-md-12 mt-4">
<h6 class="fw-bold text-success border-bottom pb-2 mb-3"><i class="fas fa-key me-2"></i>Akun Akses Login Guru</h6>
</div>
<div class="col-md-6 form-group-custom">
<label class="fw-bold small text-muted">Username Akses</label>
<input type="text" id="username" class="form-control input-custom" placeholder="Contoh: endangbk" required>
</div>
<div class="col-md-6 form-group-custom">
<label class="fw-bold small text-muted">Email Guru</label>
<input type="email" id="email" class="form-control input-custom" placeholder="Contoh: endang@sman4jember.sch.id" required>
</div>
<div class="col-md-12 form-group-custom">
<label class="fw-bold small text-muted">Password Akun</label>
<input type="password" id="password" class="form-control input-custom" placeholder="Sandi rahasia minimal 6 karakter" 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>
<button type="submit" id="btnSave" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">
Daftarkan Guru & Akun
</button>
<a href="{{ route('admin.guru') }}" class="btn btn-light px-4" style="border-radius: 10px;">
Batal
</a>
</div>
</form>
</div>
@ -31,32 +60,123 @@
</div>
<script>
document.getElementById('formCreateGuru').addEventListener('submit', async (e) => {
// Meniru Persis Logika Penentu URL Prospek Kerja
function getAppBaseUrl() {
const origin = window.location.origin;
const segments = window.location.pathname.split('/').filter(Boolean);
const adminIndex = segments.indexOf('admin');
if (adminIndex > 0) {
return origin + '/' + segments.slice(0, adminIndex).join('/');
}
return origin;
}
const APP_BASE_URL = getAppBaseUrl();
const API_GURU_URL = `${APP_BASE_URL}/api/admin/guru`;
const LOGIN_URL = `${APP_BASE_URL}/login`;
const GURU_INDEX_URL = `${APP_BASE_URL}/admin/guru`;
function getToken() {
return localStorage.getItem('access_token') ||
localStorage.getItem('token') ||
sessionStorage.getItem('access_token') ||
sessionStorage.getItem('token') ||
'';
}
function getValue(id) {
return document.getElementById(id).value.trim();
}
async function parseJsonResponse(response) {
const text = await response.text();
try {
return text ? JSON.parse(text) : {};
} catch (error) {
return {
success: false,
message: text || response.statusText || 'Respon server tidak valid.'
};
}
}
function formatErrors(errors) {
if (!errors) return '';
return Object.values(errors)
.flat()
.map(item => String(item))
.join('<br>');
}
// Meniru Persis Logika Submit Event Prospek Kerja
document.getElementById('formCreateGuru').addEventListener('submit', async function (e) {
e.preventDefault();
const token = getToken();
if (!token) {
Swal.fire('Sesi Berakhir', 'Silakan login ulang.', 'warning')
.then(() => window.location.href = LOGIN_URL);
return;
}
const btn = document.getElementById('btnSave');
btn.disabled = true;
btn.innerText = 'Menyimpan...';
const payload = {
nama_guru: document.getElementById('nama_guru').value
nama_guru: getValue('nama_guru'),
username: getValue('username'),
email: getValue('email'),
password: document.getElementById('password').value
};
try {
const response = await fetch('/api/admin/guru', {
const response = await fetch(API_GURU_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('access_token'),
'Authorization': 'Bearer ' + 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') }}"; });
const result = await parseJsonResponse(response);
if (response.status === 401 || response.status === 403) {
Swal.fire('Sesi Berakhir', 'Silakan login ulang.', 'warning')
.then(() => window.location.href = LOGIN_URL);
return;
}
} catch (error) { Swal.fire('Error!', 'Gagal menghubungi server.', 'error'); }
finally { btn.disabled = false; }
if (response.ok && result.success !== false) {
Swal.fire({
icon: 'success',
title: 'Berhasil!',
text: result.message || 'Data guru dan akun login berhasil disimpan.',
confirmButtonColor: '#2D6A4F'
}).then(() => {
window.location.href = GURU_INDEX_URL;
});
} else {
Swal.fire({
icon: 'warning',
title: 'Gagal!',
html: formatErrors(result.errors) || result.message || 'Periksa kembali isian Anda.',
confirmButtonColor: '#2D6A4F'
});
}
} catch (error) {
Swal.fire('Error!', error.message || 'Gagal menghubungi server.', 'error');
} finally {
btn.disabled = false;
btn.innerText = 'Daftarkan Guru & Akun';
}
});
</script>
@endsection

View File

@ -15,15 +15,22 @@
<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>
<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>
@ -32,47 +39,155 @@
</div>
<script>
const dbId = document.getElementById('guru_id_db').value;
function getAppBaseUrl() {
const origin = window.location.origin;
const segments = window.location.pathname.split('/').filter(Boolean);
const adminIndex = segments.indexOf('admin');
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); }
if (adminIndex > 0) {
return origin + '/' + segments.slice(0, adminIndex).join('/');
}
return origin;
}
document.getElementById('formEditGuru').addEventListener('submit', async (e) => {
const APP_BASE_URL = getAppBaseUrl();
const API_GURU_URL = `${APP_BASE_URL}/api/admin/guru`;
const LOGIN_URL = `${APP_BASE_URL}/login`;
const GURU_INDEX_URL = `${APP_BASE_URL}/admin/guru`;
const dbId = document.getElementById('guru_id_db').value;
function getToken() {
return localStorage.getItem('access_token') ||
localStorage.getItem('token') ||
sessionStorage.getItem('access_token') ||
sessionStorage.getItem('token') ||
'';
}
function getValue(id) {
return document.getElementById(id).value.trim();
}
async function parseJsonResponse(response) {
const text = await response.text();
try {
return text ? JSON.parse(text) : {};
} catch (error) {
return {
success: false,
message: text || response.statusText || 'Respon server tidak valid.'
};
}
}
function formatErrors(errors) {
if (!errors) return '';
return Object.values(errors)
.flat()
.map(item => String(item))
.join('<br>');
}
async function loadGuru() {
const token = getToken();
if (!token) {
window.location.href = LOGIN_URL;
return;
}
try {
const response = await fetch(`${API_GURU_URL}/${encodeURIComponent(dbId)}`, {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + token,
'Accept': 'application/json'
}
});
const result = await parseJsonResponse(response);
if (response.status === 401 || response.status === 403) {
Swal.fire('Sesi Berakhir', 'Silakan login ulang.', 'warning')
.then(() => window.location.href = LOGIN_URL);
return;
}
if (response.ok && result.success !== false && result.data) {
document.getElementById('nama_guru').value = result.data.nama_guru || '';
document.getElementById('display_id_guru').innerText = `Ubah profil guru BK (${result.data.id_guru || '-'}).`;
} else {
Swal.fire('Gagal!', result.message || 'Data guru tidak ditemukan.', 'warning');
}
} catch (error) {
Swal.fire('Error!', error.message || 'Gagal mengambil data dari server.', 'error');
}
}
document.getElementById('formEditGuru').addEventListener('submit', async function (e) {
e.preventDefault();
const token = getToken();
if (!token) {
Swal.fire('Sesi Berakhir', 'Silakan login ulang.', 'warning')
.then(() => window.location.href = LOGIN_URL);
return;
}
const btn = document.getElementById('btnUpdate');
btn.disabled = true;
btn.innerText = 'Memperbarui...';
const payload = {
nama_guru: document.getElementById('nama_guru').value
nama_guru: getValue('nama_guru')
};
try {
const response = await fetch(`/api/admin/guru/${dbId}`, {
const response = await fetch(`${API_GURU_URL}/${encodeURIComponent(dbId)}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('access_token'),
'Authorization': 'Bearer ' + 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') }}"; });
const result = await parseJsonResponse(response);
if (response.status === 401 || response.status === 403) {
Swal.fire('Sesi Berakhir', 'Silakan login ulang.', 'warning')
.then(() => window.location.href = LOGIN_URL);
return;
}
} catch (error) { Swal.fire('Error!', 'Gagal koneksi.', 'error'); }
finally { btn.disabled = false; }
if (response.ok && result.success !== false) {
Swal.fire({
icon: 'success',
title: 'Berhasil Diperbarui!',
text: result.message || 'Data guru berhasil diperbarui.',
confirmButtonColor: '#2D7A47'
}).then(() => {
window.location.href = GURU_INDEX_URL;
});
} else {
Swal.fire({
icon: 'warning',
title: 'Gagal!',
html: formatErrors(result.errors) || result.message || 'Cek kembali data yang dimasukkan.',
confirmButtonColor: '#2D7A47'
});
}
} catch (error) {
Swal.fire('Error!', error.message || 'Kesalahan koneksi server.', 'error');
} finally {
btn.disabled = false;
btn.innerText = 'Update Profil';
}
});
document.addEventListener('DOMContentLoaded', loadGuru);

View File

@ -21,7 +21,7 @@
<div class="table-toolbar">
<div class="search-wrapper-modern">
<i class="fas fa-search"></i>
<input type="text" id="searchInput" class="search-input-modern" placeholder="Cari nama jurusan, rumpun, atau prospek karir...">
<input type="text" id="searchInput" class="search-input-modern" placeholder="Cari nama jurusan, rumpun, atau prospek kerja...">
</div>
<div class="per-page-wrapper">
@ -39,15 +39,15 @@
<tr>
<th class="text-center" width="70">No</th>
<th>Nama Jurusan</th>
<th class="text-center">Rumpun</th>
<th>Prospek Karir</th>
<th>Rumpun</th>
<th>Prospek Kerja</th>
<th class="text-center" width="150">Opsi</th>
</tr>
</thead>
<tbody id="jurusan-table-body">
<tr>
<td colspan="5" class="loading-state-modern">
<i class="fas fa-spinner fa-spin me-2"></i> Memuat data jurusan...
<i class="fas fa-spinner fa-spin me-2"></i> Memuat data...
</td>
</tr>
</tbody>
@ -64,13 +64,23 @@
</div>
<script>
const API_JURUSAN_URL = "{{ url('/api/admin/jurusan') }}";
let currentPage = 1;
let currentSearch = '';
let perPage = 7;
let currentSearch = '';
let searchTimer = null;
function getToken() {
return localStorage.getItem('access_token') ||
localStorage.getItem('token') ||
sessionStorage.getItem('access_token') ||
sessionStorage.getItem('token') ||
'';
}
function escapeHtml(value) {
if (value === null || value === undefined || value === '') return '-';
return String(value)
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
@ -79,27 +89,34 @@ function escapeHtml(value) {
.replaceAll("'", '&#039;');
}
function showLoading() {
document.getElementById('jurusan-table-body').innerHTML = `
<tr>
<td colspan="5" class="loading-state-modern">
<i class="fas fa-spinner fa-spin me-2"></i> Memuat data jurusan...
</td>
</tr>
`;
async function parseJsonResponse(response) {
const text = await response.text();
try {
return text ? JSON.parse(text) : {};
} catch (e) {
return {
success: false,
message: text || response.statusText || 'Respon server tidak valid.'
};
}
}
async function fetchJurusan(page = 1) {
const token = localStorage.getItem('access_token');
const token = getToken();
const tbody = document.getElementById('jurusan-table-body');
if (!token) {
window.location.href = '/login';
return;
}
currentPage = page;
showLoading();
tbody.innerHTML = `
<tr>
<td colspan="5" class="loading-state-modern">
<i class="fas fa-spinner fa-spin me-2"></i> Memuat data...
</td>
</tr>
`;
const params = new URLSearchParams({
page: page,
@ -111,14 +128,15 @@ function showLoading() {
}
try {
const response = await fetch(`/api/admin/jurusan?${params.toString()}`, {
const response = await fetch(`${API_JURUSAN_URL}?${params.toString()}`, {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + token,
'Accept': 'application/json'
}
});
const result = await response.json();
const result = await parseJsonResponse(response);
if (!response.ok || result.success === false) {
throw new Error(result.message || 'Gagal memuat data jurusan.');
@ -136,39 +154,53 @@ function showLoading() {
</td>
</tr>
`;
updatePaginationInfo(null);
renderPagination({ current_page: 1, last_page: 1, total: 0 });
return;
}
const startNo = result.from || 1;
data.forEach((item, index) => {
const rumpun = item.rumpun || '-';
const rumpunClass = rumpun.toLowerCase() === 'saintek' ? 'badge-saintek' : 'badge-soshum';
const editUrl = `{{ url('/admin/jurusan') }}/${encodeURIComponent(item.id)}/edit`;
let prospekText = '-';
if (item.prospeks && item.prospeks.length > 0) {
prospekText = item.prospeks.map(p => escapeHtml(p.nama_profesi)).join(', ');
}
tbody.innerHTML += `
<tr>
<td class="text-center text-muted fw-bold">${(result.from || 1) + index}</td>
<td class="text-center text-muted fw-bold">${startNo + index}</td>
<td>
<div class="table-user-cell">
<div class="avatar-circle purple">
<i class="fas fa-graduation-cap"></i>
</div>
<div>
<div class="fw-bold text-dark text-uppercase">${escapeHtml(item.nama_jurusan)}</div>
<div class="fw-bold text-dark">${escapeHtml(item.nama_jurusan)}</div>
<div class="text-cell-muted">Data master jurusan</div>
</div>
</div>
</td>
<td class="text-center">
<span class="badge-soft ${rumpunClass}">${escapeHtml(rumpun)}</span>
<td>
<span class="badge-soft badge-result">${escapeHtml(item.rumpun)}</span>
</td>
<td class="text-cell-muted">${escapeHtml(item.prospek_karir)}</td>
<td>
<span class="text-muted small fw-medium">${prospekText}</span>
</td>
<td class="text-center">
<div class="opsi-wrapper">
<a href="/admin/jurusan/${item.id}/edit" class="btn-action btn-edit" title="Edit">
<a href="${editUrl}" 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">
<button type="button" onclick="deleteJurusan(${item.id}, '${escapeHtml(item.nama_jurusan)}')" class="btn-action btn-delete" title="Hapus">
<i class="fas fa-trash"></i>
</button>
</div>
@ -185,7 +217,7 @@ function showLoading() {
<tr>
<td colspan="5" class="empty-state-modern text-danger">
<i class="fas fa-exclamation-circle"></i>
Gagal memuat data jurusan.
${escapeHtml(error.message || 'Gagal memuat data jurusan.')}
</td>
</tr>
`;
@ -218,24 +250,32 @@ function renderPagination(data) {
btn.className = `page-link-custom ${active ? 'active' : ''}`;
btn.innerHTML = label;
btn.disabled = disabled;
btn.onclick = () => {
if (!disabled && !active) fetchJurusan(page);
};
return btn;
};
controls.appendChild(createButton('<i class="fas fa-chevron-left"></i>', current - 1, current === 1));
let pages = [];
if (last <= 5) {
for (let i = 1; i <= last; i++) pages.push(i);
} else {
pages.push(1);
if (current > 3) pages.push('...');
const start = Math.max(2, current - 1);
const end = Math.min(last - 1, current + 1);
for (let i = start; i <= end; i++) pages.push(i);
if (current < last - 2) pages.push('...');
pages.push(last);
}
@ -250,10 +290,10 @@ function renderPagination(data) {
controls.appendChild(createButton('<i class="fas fa-chevron-right"></i>', current + 1, current === last));
}
async function deleteJurusan(id) {
const result = await Swal.fire({
async function deleteJurusan(id, name) {
const confirmResult = await Swal.fire({
title: 'Hapus data?',
text: 'Data jurusan yang dihapus tidak dapat dikembalikan!',
text: `Data jurusan "${name}" beserta prospek kerjanya akan dihapus permanen!`,
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#ef4444',
@ -262,12 +302,12 @@ function renderPagination(data) {
cancelButtonText: 'Batal'
});
if (!result.isConfirmed) return;
if (!confirmResult.isConfirmed) return;
const token = localStorage.getItem('access_token');
const token = getToken();
try {
const response = await fetch('/api/admin/jurusan/' + id, {
const response = await fetch(`${API_JURUSAN_URL}/${encodeURIComponent(id)}`, {
method: 'DELETE',
headers: {
'Authorization': 'Bearer ' + token,
@ -275,14 +315,16 @@ function renderPagination(data) {
}
});
if (response.ok) {
Swal.fire('Terhapus!', 'Data jurusan berhasil dihapus.', 'success');
const resultDelete = await parseJsonResponse(response);
if (response.ok && resultDelete.success !== false) {
Swal.fire('Terhapus!', resultDelete.message || 'Data jurusan berhasil dihapus.', 'success');
fetchJurusan(currentPage);
} else {
Swal.fire('Gagal!', 'Data jurusan gagal dihapus.', 'error');
Swal.fire('Gagal!', resultDelete.message || 'Data jurusan gagal dihapus.', 'error');
}
} catch (error) {
Swal.fire('Error!', 'Koneksi ke server gagal.', 'error');
Swal.fire('Error!', error.message || 'Koneksi ke server gagal.', 'error');
}
}
@ -291,6 +333,7 @@ function renderPagination(data) {
document.getElementById('searchInput').addEventListener('input', function () {
clearTimeout(searchTimer);
searchTimer = setTimeout(() => {
currentSearch = this.value.trim();
fetchJurusan(1);

View File

@ -19,6 +19,7 @@
<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>
@ -27,14 +28,16 @@
<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>
<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>
@ -43,34 +46,119 @@
</div>
<script>
document.getElementById('formCreateJurusan').addEventListener('submit', async (e) => {
function getAppBaseUrl() {
const origin = window.location.origin;
const segments = window.location.pathname.split('/').filter(Boolean);
const adminIndex = segments.indexOf('admin');
if (adminIndex > 0) {
return origin + '/' + segments.slice(0, adminIndex).join('/');
}
return origin;
}
const APP_BASE_URL = getAppBaseUrl();
const API_JURUSAN_URL = `${APP_BASE_URL}/api/admin/jurusan`;
const LOGIN_URL = `${APP_BASE_URL}/login`;
const JURUSAN_INDEX_URL = `${APP_BASE_URL}/admin/jurusan`;
function getToken() {
return localStorage.getItem('access_token') ||
localStorage.getItem('token') ||
sessionStorage.getItem('access_token') ||
sessionStorage.getItem('token') ||
'';
}
function getValue(id) {
return document.getElementById(id).value.trim();
}
async function parseJsonResponse(response) {
const text = await response.text();
try {
return text ? JSON.parse(text) : {};
} catch (error) {
return {
success: false,
message: text || response.statusText || 'Respon server tidak valid.'
};
}
}
function formatErrors(errors) {
if (!errors) return '';
return Object.values(errors)
.flat()
.map(item => String(item))
.join('<br>');
}
document.getElementById('formCreateJurusan').addEventListener('submit', async function (e) {
e.preventDefault();
const token = getToken();
if (!token) {
Swal.fire('Sesi Berakhir', 'Silakan login ulang.', 'warning')
.then(() => window.location.href = LOGIN_URL);
return;
}
const btn = document.getElementById('btnSave');
btn.disabled = true;
btn.innerText = 'Menyimpan...';
const payload = {
nama_jurusan: document.getElementById('nama_jurusan').value,
rumpun: document.getElementById('rumpun').value,
prospek_karir: document.getElementById('prospek_karir').value
nama_jurusan: getValue('nama_jurusan'),
rumpun: getValue('rumpun'),
};
try {
const response = await fetch('/api/admin/jurusan', {
const response = await fetch(API_JURUSAN_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('access_token'),
'Authorization': 'Bearer ' + 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') }}"; });
const result = await parseJsonResponse(response);
if (response.status === 401 || response.status === 403) {
Swal.fire('Sesi Berakhir', 'Silakan login ulang.', 'warning')
.then(() => window.location.href = LOGIN_URL);
return;
}
} catch (error) { Swal.fire('Error!', 'Gagal menghubungi server.', 'error'); }
finally { btn.disabled = false; }
if (response.ok && result.success !== false) {
Swal.fire({
icon: 'success',
title: 'Berhasil!',
text: result.message || 'Data jurusan berhasil ditambahkan.',
confirmButtonColor: '#2D7A47'
}).then(() => {
window.location.href = JURUSAN_INDEX_URL;
});
} else {
Swal.fire({
icon: 'warning',
title: 'Gagal!',
html: formatErrors(result.errors) || result.message || 'Cek kembali data yang dimasukkan.',
confirmButtonColor: '#2D7A47'
});
}
} catch (error) {
Swal.fire('Error!', error.message || 'Gagal menghubungi server.', 'error');
} finally {
btn.disabled = false;
btn.innerText = 'Simpan Data';
}
});
</script>
@endsection

View File

@ -15,11 +15,13 @@
<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>
@ -27,14 +29,16 @@
<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>
<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>
@ -43,50 +47,156 @@
</div>
<script>
const jurusanId = document.getElementById('jurusan_id').value;
function getAppBaseUrl() {
const origin = window.location.origin;
const segments = window.location.pathname.split('/').filter(Boolean);
const adminIndex = segments.indexOf('admin');
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); }
if (adminIndex > 0) {
return origin + '/' + segments.slice(0, adminIndex).join('/');
}
return origin;
}
document.getElementById('formEditJurusan').addEventListener('submit', async (e) => {
const APP_BASE_URL = getAppBaseUrl();
const API_JURUSAN_URL = `${APP_BASE_URL}/api/admin/jurusan`;
const LOGIN_URL = `${APP_BASE_URL}/login`;
const JURUSAN_INDEX_URL = `${APP_BASE_URL}/admin/jurusan`;
const jurusanId = document.getElementById('jurusan_id').value;
function getToken() {
return localStorage.getItem('access_token') ||
localStorage.getItem('token') ||
sessionStorage.getItem('access_token') ||
sessionStorage.getItem('token') ||
'';
}
function getValue(id) {
return document.getElementById(id).value.trim();
}
async function parseJsonResponse(response) {
const text = await response.text();
try {
return text ? JSON.parse(text) : {};
} catch (error) {
return {
success: false,
message: text || response.statusText || 'Respon server tidak valid.'
};
}
}
function formatErrors(errors) {
if (!errors) return '';
return Object.values(errors)
.flat()
.map(item => String(item))
.join('<br>');
}
async function loadData() {
const token = getToken();
if (!token) {
window.location.href = LOGIN_URL;
return;
}
try {
const response = await fetch(`${API_JURUSAN_URL}/${encodeURIComponent(jurusanId)}`, {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + token,
'Accept': 'application/json'
}
});
const result = await parseJsonResponse(response);
if (response.status === 401 || response.status === 403) {
Swal.fire('Sesi Berakhir', 'Silakan login ulang.', 'warning')
.then(() => window.location.href = LOGIN_URL);
return;
}
if (response.ok && result.success !== false && result.data) {
document.getElementById('nama_jurusan').value = result.data.nama_jurusan || '';
document.getElementById('rumpun').value = result.data.rumpun || '';
} else {
Swal.fire('Gagal!', result.message || 'Data jurusan tidak ditemukan.', 'warning');
}
} catch (error) {
Swal.fire('Error!', error.message || 'Gagal mengambil data dari server.', 'error');
}
}
document.getElementById('formEditJurusan').addEventListener('submit', async function (e) {
e.preventDefault();
const token = getToken();
if (!token) {
Swal.fire('Sesi Berakhir', 'Silakan login ulang.', 'warning')
.then(() => window.location.href = LOGIN_URL);
return;
}
const btn = document.getElementById('btnUpdate');
btn.disabled = true;
btn.innerText = 'Memperbarui...';
const payload = {
nama_jurusan: document.getElementById('nama_jurusan').value,
rumpun: document.getElementById('rumpun').value,
prospek_karir: document.getElementById('prospek_karir').value
nama_jurusan: getValue('nama_jurusan'),
rumpun: getValue('rumpun'),
};
try {
const response = await fetch(`/api/admin/jurusan/${jurusanId}`, {
const response = await fetch(`${API_JURUSAN_URL}/${encodeURIComponent(jurusanId)}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('access_token'),
'Authorization': 'Bearer ' + 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') }}"; });
const result = await parseJsonResponse(response);
if (response.status === 401 || response.status === 403) {
Swal.fire('Sesi Berakhir', 'Silakan login ulang.', 'warning')
.then(() => window.location.href = LOGIN_URL);
return;
}
} catch (error) { Swal.fire('Error!', 'Gagal koneksi.', 'error'); }
finally { btn.disabled = false; }
if (response.ok && result.success !== false) {
Swal.fire({
icon: 'success',
title: 'Berhasil Diperbarui!',
text: result.message || 'Data jurusan berhasil diperbarui.',
confirmButtonColor: '#2D7A47'
}).then(() => {
window.location.href = JURUSAN_INDEX_URL;
});
} else {
Swal.fire({
icon: 'warning',
title: 'Gagal!',
html: formatErrors(result.errors) || result.message || 'Cek kembali data yang dimasukkan.',
confirmButtonColor: '#2D7A47'
});
}
} catch (error) {
Swal.fire('Error!', error.message || 'Kesalahan koneksi server.', 'error');
} finally {
btn.disabled = false;
btn.innerText = 'Perbarui Data';
}
});
document.addEventListener('DOMContentLoaded', loadData);

View File

@ -44,6 +44,7 @@
<th class="text-center" width="150">Opsi</th>
</tr>
</thead>
<tbody id="prospek-table-body">
<tr>
<td colspan="5" class="loading-state-modern">
@ -58,19 +59,33 @@
<div class="text-muted small" id="pagination-info">
Menampilkan 0 sampai 0 dari 0 data
</div>
<div class="d-flex gap-1 flex-wrap" id="pagination-controls"></div>
</div>
</div>
</div>
<script>
const API_PROSPEK_URL = @json(url('/api/admin/prospek-kerja'));
const LOGIN_URL = @json(url('/login'));
const PROSPEK_EDIT_BASE_URL = @json(url('/admin/prospek-kerja'));
let currentPage = 1;
let currentSearch = '';
let perPage = 7;
let searchTimer = null;
function getToken() {
return localStorage.getItem('access_token') ||
localStorage.getItem('token') ||
sessionStorage.getItem('access_token') ||
sessionStorage.getItem('token') ||
'';
}
function escapeHtml(value) {
if (value === null || value === undefined || value === '') return '-';
return String(value)
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
@ -79,6 +94,19 @@ function escapeHtml(value) {
.replaceAll("'", '&#039;');
}
async function parseJsonResponse(response) {
const text = await response.text();
try {
return text ? JSON.parse(text) : {};
} catch (error) {
return {
success: false,
message: text || response.statusText || 'Respon server tidak valid.'
};
}
}
function showLoading() {
document.getElementById('prospek-table-body').innerHTML = `
<tr>
@ -90,11 +118,11 @@ function showLoading() {
}
async function fetchProspek(page = 1) {
const token = localStorage.getItem('access_token');
const token = getToken();
const tbody = document.getElementById('prospek-table-body');
if (!token) {
window.location.href = '/login';
window.location.href = LOGIN_URL;
return;
}
@ -111,14 +139,27 @@ function showLoading() {
}
try {
const response = await fetch(`/api/admin/prospek-kerja?${params.toString()}`, {
const response = await fetch(`${API_PROSPEK_URL}?${params.toString()}`, {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + token,
'Accept': 'application/json'
}
});
const result = await response.json();
const result = await parseJsonResponse(response);
if (response.status === 401 || response.status === 403) {
Swal.fire('Sesi Berakhir', 'Silakan login ulang.', 'warning')
.then(() => {
localStorage.removeItem('access_token');
localStorage.removeItem('token');
sessionStorage.removeItem('access_token');
sessionStorage.removeItem('token');
window.location.href = LOGIN_URL;
});
return;
}
if (!response.ok || result.success === false) {
throw new Error(result.message || 'Gagal memuat data prospek kerja.');
@ -136,6 +177,7 @@ function showLoading() {
</td>
</tr>
`;
updatePaginationInfo(null);
renderPagination({ current_page: 1, last_page: 1, total: 0 });
return;
@ -143,31 +185,38 @@ function showLoading() {
data.forEach((item, index) => {
const namaJurusan = item.jurusan ? escapeHtml(item.jurusan.nama_jurusan) : 'Jurusan Dihapus';
const editUrl = `${PROSPEK_EDIT_BASE_URL}/${encodeURIComponent(item.id)}/edit`;
tbody.innerHTML += `
<tr>
<td class="text-center text-muted fw-bold">${(result.from || 1) + index}</td>
<td>
<div class="table-user-cell">
<div class="avatar-circle blue">
<i class="fas fa-briefcase"></i>
</div>
<div>
<div class="fw-bold text-dark">${escapeHtml(item.nama_profesi)}</div>
<div class="text-cell-muted">Prospek kerja</div>
</div>
</div>
</td>
<td>
<span class="badge-soft badge-result">${namaJurusan}</span>
</td>
<td class="fw-bold text-success">${escapeHtml(item.range_gaji || 'Belum diatur')}</td>
<td class="text-center">
<div class="opsi-wrapper">
<a href="/admin/prospek-kerja/${item.id}/edit" class="btn-action btn-edit" title="Edit">
<a href="${editUrl}" 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">
<button type="button" onclick="deleteProspek(${item.id})" class="btn-action btn-delete" title="Hapus">
<i class="fas fa-trash"></i>
</button>
</div>
@ -184,7 +233,7 @@ function showLoading() {
<tr>
<td colspan="5" class="empty-state-modern text-danger">
<i class="fas fa-exclamation-circle"></i>
Gagal memuat data prospek kerja.
${escapeHtml(error.message || 'Gagal memuat data prospek kerja.')}
</td>
</tr>
`;
@ -217,24 +266,40 @@ function renderPagination(data) {
btn.className = `page-link-custom ${active ? 'active' : ''}`;
btn.innerHTML = label;
btn.disabled = disabled;
btn.onclick = () => {
if (!disabled && !active) fetchProspek(page);
};
return btn;
};
controls.appendChild(createButton('<i class="fas fa-chevron-left"></i>', current - 1, current === 1));
let pages = [];
if (last <= 5) {
for (let i = 1; i <= last; i++) pages.push(i);
for (let i = 1; i <= last; i++) {
pages.push(i);
}
} else {
pages.push(1);
if (current > 3) pages.push('...');
if (current > 3) {
pages.push('...');
}
const start = Math.max(2, current - 1);
const end = Math.min(last - 1, current + 1);
for (let i = start; i <= end; i++) pages.push(i);
if (current < last - 2) pages.push('...');
for (let i = start; i <= end; i++) {
pages.push(i);
}
if (current < last - 2) {
pages.push('...');
}
pages.push(last);
}
@ -263,10 +328,10 @@ function renderPagination(data) {
if (!result.isConfirmed) return;
const token = localStorage.getItem('access_token');
const token = getToken();
try {
const response = await fetch('/api/admin/prospek-kerja/' + id, {
const response = await fetch(`${API_PROSPEK_URL}/${encodeURIComponent(id)}`, {
method: 'DELETE',
headers: {
'Authorization': 'Bearer ' + token,
@ -274,14 +339,16 @@ function renderPagination(data) {
}
});
if (response.ok) {
Swal.fire('Terhapus!', 'Data prospek kerja berhasil dihapus.', 'success');
const resultDelete = await parseJsonResponse(response);
if (response.ok && resultDelete.success !== false) {
Swal.fire('Terhapus!', resultDelete.message || 'Data prospek kerja berhasil dihapus.', 'success');
fetchProspek(currentPage);
} else {
Swal.fire('Gagal!', 'Data prospek kerja gagal dihapus.', 'error');
Swal.fire('Gagal!', resultDelete.message || 'Data prospek kerja gagal dihapus.', 'error');
}
} catch (error) {
Swal.fire('Error!', 'Koneksi ke server gagal.', 'error');
Swal.fire('Error!', error.message || 'Koneksi ke server gagal.', 'error');
}
}
@ -290,6 +357,7 @@ function renderPagination(data) {
document.getElementById('searchInput').addEventListener('input', function () {
clearTimeout(searchTimer);
searchTimer = setTimeout(() => {
currentSearch = this.value.trim();
fetchProspek(1);

View File

@ -21,22 +21,31 @@
<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>
<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>
@ -45,54 +54,180 @@
</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>';
function getAppBaseUrl() {
const origin = window.location.origin;
const segments = window.location.pathname.split('/').filter(Boolean);
const adminIndex = segments.indexOf('admin');
result.data.forEach(item => {
select.innerHTML += `<option value="${item.id}">${item.nama_jurusan}</option>`;
});
if (adminIndex > 0) {
return origin + '/' + segments.slice(0, adminIndex).join('/');
}
return origin;
}
const APP_BASE_URL = getAppBaseUrl();
const API_PROSPEK_URL = `${APP_BASE_URL}/api/admin/prospek-kerja`;
const API_JURUSAN_URL = `${APP_BASE_URL}/api/admin/jurusan`;
const LOGIN_URL = `${APP_BASE_URL}/login`;
const PROSPEK_INDEX_URL = `${APP_BASE_URL}/admin/prospek-kerja`;
function getToken() {
return localStorage.getItem('access_token') ||
localStorage.getItem('token') ||
sessionStorage.getItem('access_token') ||
sessionStorage.getItem('token') ||
'';
}
function getValue(id) {
return document.getElementById(id).value.trim();
}
function escapeHtml(value) {
if (value === null || value === undefined || value === '') return '-';
return String(value)
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#039;');
}
async function parseJsonResponse(response) {
const text = await response.text();
try {
return text ? JSON.parse(text) : {};
} catch (error) {
document.getElementById('jurusan_id').innerHTML = '<option disabled>Gagal memuat jurusan</option>';
return {
success: false,
message: text || response.statusText || 'Respon server tidak valid.'
};
}
}
document.getElementById('formCreateProspek').addEventListener('submit', async (e) => {
function formatErrors(errors) {
if (!errors) return '';
return Object.values(errors)
.flat()
.map(item => String(item))
.join('<br>');
}
async function loadJurusan() {
const token = getToken();
const select = document.getElementById('jurusan_id');
if (!token) {
window.location.href = LOGIN_URL;
return;
}
try {
const response = await fetch(`${API_JURUSAN_URL}?per_page=100`, {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + token,
'Accept': 'application/json'
}
});
const result = await parseJsonResponse(response);
select.innerHTML = '<option value="" disabled selected>Pilih Jurusan Terkait...</option>';
if (response.status === 401 || response.status === 403) {
window.location.href = LOGIN_URL;
return;
}
if (!response.ok || result.success === false) {
throw new Error(result.message || 'Gagal memuat data jurusan.');
}
const data = result.data || [];
if (data.length === 0) {
select.innerHTML = '<option value="" disabled selected>Belum ada data jurusan</option>';
return;
}
data.forEach(item => {
select.innerHTML += `<option value="${escapeHtml(item.id)}">${escapeHtml(item.nama_jurusan)}</option>`;
});
} catch (error) {
select.innerHTML = '<option value="" disabled selected>Gagal memuat jurusan</option>';
Swal.fire('Error!', error.message || 'Gagal memuat daftar jurusan.', 'error');
}
}
document.getElementById('formCreateProspek').addEventListener('submit', async function (e) {
e.preventDefault();
const token = getToken();
if (!token) {
Swal.fire('Sesi Berakhir', 'Silakan login ulang.', 'warning')
.then(() => window.location.href = LOGIN_URL);
return;
}
const btn = document.getElementById('btnSave');
btn.disabled = true;
btn.innerText = 'Menyimpan...';
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
jurusan_id: getValue('jurusan_id'),
nama_profesi: getValue('nama_profesi'),
range_gaji: getValue('range_gaji'),
deskripsi_pekerjaan: getValue('deskripsi_pekerjaan')
};
try {
const response = await fetch('/api/admin/prospek-kerja', {
const response = await fetch(API_PROSPEK_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('access_token'),
'Authorization': 'Bearer ' + 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');
const result = await parseJsonResponse(response);
if (response.status === 401 || response.status === 403) {
Swal.fire('Sesi Berakhir', 'Silakan login ulang.', 'warning')
.then(() => window.location.href = LOGIN_URL);
return;
}
} catch (error) { Swal.fire('Error!', 'Gagal menghubungi server.', 'error'); }
finally { btn.disabled = false; }
if (response.ok && result.success !== false) {
Swal.fire({
icon: 'success',
title: 'Berhasil!',
text: result.message || 'Data prospek kerja berhasil ditambahkan.',
confirmButtonColor: '#2D6A4F'
}).then(() => {
window.location.href = PROSPEK_INDEX_URL;
});
} else {
Swal.fire({
icon: 'warning',
title: 'Gagal!',
html: formatErrors(result.errors) || result.message || 'Periksa kembali isian Anda.',
confirmButtonColor: '#2D6A4F'
});
}
} catch (error) {
Swal.fire('Error!', error.message || 'Gagal menghubungi server.', 'error');
} finally {
btn.disabled = false;
btn.innerText = 'Simpan Data';
}
});
document.addEventListener('DOMContentLoaded', loadJurusan);

View File

@ -15,6 +15,7 @@
<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>
@ -22,22 +23,31 @@
<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>
<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>
@ -46,66 +56,212 @@
</div>
<script>
const prospekId = document.getElementById('prospek_id').value;
function getAppBaseUrl() {
const origin = window.location.origin;
const segments = window.location.pathname.split('/').filter(Boolean);
const adminIndex = segments.indexOf('admin');
async function loadJurusanAndData() {
const token = localStorage.getItem('access_token');
if (adminIndex > 0) {
return origin + '/' + segments.slice(0, adminIndex).join('/');
}
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); }
return origin;
}
document.getElementById('formEditProspek').addEventListener('submit', async (e) => {
const APP_BASE_URL = getAppBaseUrl();
const API_PROSPEK_URL = `${APP_BASE_URL}/api/admin/prospek-kerja`;
const API_JURUSAN_URL = `${APP_BASE_URL}/api/admin/jurusan`;
const LOGIN_URL = `${APP_BASE_URL}/login`;
const PROSPEK_INDEX_URL = `${APP_BASE_URL}/admin/prospek-kerja`;
const prospekId = document.getElementById('prospek_id').value;
function getToken() {
return localStorage.getItem('access_token') ||
localStorage.getItem('token') ||
sessionStorage.getItem('access_token') ||
sessionStorage.getItem('token') ||
'';
}
function getValue(id) {
return document.getElementById(id).value.trim();
}
function escapeHtml(value) {
if (value === null || value === undefined || value === '') return '-';
return String(value)
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#039;');
}
async function parseJsonResponse(response) {
const text = await response.text();
try {
return text ? JSON.parse(text) : {};
} catch (error) {
return {
success: false,
message: text || response.statusText || 'Respon server tidak valid.'
};
}
}
function formatErrors(errors) {
if (!errors) return '';
return Object.values(errors)
.flat()
.map(item => String(item))
.join('<br>');
}
async function loadJurusanOptions(selectedId = '') {
const token = getToken();
const select = document.getElementById('jurusan_id');
const response = await fetch(`${API_JURUSAN_URL}?per_page=100`, {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + token,
'Accept': 'application/json'
}
});
const result = await parseJsonResponse(response);
select.innerHTML = '<option value="" disabled>Pilih Jurusan Terkait...</option>';
if (response.status === 401 || response.status === 403) {
window.location.href = LOGIN_URL;
return;
}
if (!response.ok || result.success === false) {
throw new Error(result.message || 'Gagal memuat data jurusan.');
}
const data = result.data || [];
if (data.length === 0) {
select.innerHTML = '<option value="" disabled>Belum ada data jurusan</option>';
return;
}
data.forEach(item => {
const selected = String(item.id) === String(selectedId) ? 'selected' : '';
select.innerHTML += `<option value="${escapeHtml(item.id)}" ${selected}>${escapeHtml(item.nama_jurusan)}</option>`;
});
}
async function loadJurusanAndData() {
const token = getToken();
if (!token) {
window.location.href = LOGIN_URL;
return;
}
try {
const resProspek = await fetch(`${API_PROSPEK_URL}/${encodeURIComponent(prospekId)}`, {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + token,
'Accept': 'application/json'
}
});
const resultProspek = await parseJsonResponse(resProspek);
if (resProspek.status === 401 || resProspek.status === 403) {
window.location.href = LOGIN_URL;
return;
}
if (!resProspek.ok || resultProspek.success === false || !resultProspek.data) {
throw new Error(resultProspek.message || 'Data prospek kerja tidak ditemukan.');
}
await loadJurusanOptions(resultProspek.data.jurusan_id);
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) {
Swal.fire('Error!', error.message || 'Gagal memuat data prospek kerja.', 'error');
}
}
document.getElementById('formEditProspek').addEventListener('submit', async function (e) {
e.preventDefault();
const token = getToken();
if (!token) {
Swal.fire('Sesi Berakhir', 'Silakan login ulang.', 'warning')
.then(() => window.location.href = LOGIN_URL);
return;
}
const btn = document.getElementById('btnUpdate');
btn.disabled = true;
btn.innerText = 'Memperbarui...';
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
jurusan_id: getValue('jurusan_id'),
nama_profesi: getValue('nama_profesi'),
range_gaji: getValue('range_gaji'),
deskripsi_pekerjaan: getValue('deskripsi_pekerjaan')
};
try {
const response = await fetch(`/api/admin/prospek-kerja/${prospekId}`, {
const response = await fetch(`${API_PROSPEK_URL}/${encodeURIComponent(prospekId)}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('access_token'),
'Authorization': 'Bearer ' + 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') }}"; });
const result = await parseJsonResponse(response);
if (response.status === 401 || response.status === 403) {
Swal.fire('Sesi Berakhir', 'Silakan login ulang.', 'warning')
.then(() => window.location.href = LOGIN_URL);
return;
}
} catch (error) { Swal.fire('Error!', 'Gagal koneksi.', 'error'); }
finally { btn.disabled = false; }
if (response.ok && result.success !== false) {
Swal.fire({
icon: 'success',
title: 'Berhasil Diperbarui!',
text: result.message || 'Data prospek kerja berhasil diperbarui.',
confirmButtonColor: '#2D6A4F'
}).then(() => {
window.location.href = PROSPEK_INDEX_URL;
});
} else {
Swal.fire({
icon: 'warning',
title: 'Gagal!',
html: formatErrors(result.errors) || result.message || 'Periksa kembali isian Anda.',
confirmButtonColor: '#2D6A4F'
});
}
} catch (error) {
Swal.fire('Error!', error.message || 'Gagal koneksi ke server.', 'error');
} finally {
btn.disabled = false;
btn.innerText = 'Perbarui Data';
}
});
document.addEventListener('DOMContentLoaded', loadJurusanAndData);

View File

@ -94,13 +94,36 @@
</div>
</div>
@push('scripts')
<script>
function getAppBaseUrl() {
const origin = window.location.origin;
const segments = window.location.pathname.split('/').filter(Boolean);
const adminIndex = segments.indexOf('admin');
if (adminIndex > 0) {
return origin + '/' + segments.slice(0, adminIndex).join('/');
}
return origin;
}
const APP_BASE_URL = getAppBaseUrl();
const API_RIWAYAT_URL = `${APP_BASE_URL}/api/admin/riwayat`;
const LOGIN_URL = `${APP_BASE_URL}/login`;
let currentPage = 1;
let currentSearch = '';
let perPage = 7;
let searchTimer = null;
function getToken() {
return localStorage.getItem('access_token') ||
localStorage.getItem('token') ||
sessionStorage.getItem('access_token') ||
sessionStorage.getItem('token') ||
'';
}
function escapeHtml(value) {
if (value === null || value === undefined || value === '') return '-';
@ -112,6 +135,19 @@ function escapeHtml(value) {
.replaceAll("'", '&#039;');
}
async function parseJsonResponse(response) {
const text = await response.text();
try {
return text ? JSON.parse(text) : {};
} catch (error) {
return {
success: false,
message: text || response.statusText || 'Respon server tidak valid.'
};
}
}
function formatPercent(value) {
if (value === null || value === undefined || value === '') {
return '-';
@ -152,11 +188,11 @@ function showLoading() {
}
async function fetchRiwayat(page = 1) {
const token = localStorage.getItem('access_token');
const token = getToken();
const tbody = document.getElementById('table-riwayat');
if (!token) {
window.location.href = '/login';
window.location.href = LOGIN_URL;
return;
}
@ -173,14 +209,20 @@ function showLoading() {
}
try {
const response = await fetch(`/api/admin/riwayat?${params.toString()}`, {
const response = await fetch(`${API_RIWAYAT_URL}?${params.toString()}`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/json'
}
});
const result = await response.json();
const result = await parseJsonResponse(response);
if (response.status === 401 || response.status === 403) {
window.location.href = LOGIN_URL;
return;
}
if (!response.ok || result.success === false) {
throw new Error(result.message || 'Gagal memuat data riwayat.');
@ -219,6 +261,7 @@ function showLoading() {
<td>
<div class="riwayat-user-cell">
<div class="riwayat-avatar">${escapeHtml(getInitial(item.nama_siswa))}</div>
<div>
<div class="riwayat-user-name">${escapeHtml(item.nama_siswa)}</div>
<div class="riwayat-user-email">${escapeHtml(item.email_siswa)}</div>
@ -245,7 +288,7 @@ function showLoading() {
</td>
<td class="text-center">
<button class="btn-detail" onclick="showDetail(${item.id})">
<button type="button" class="btn-detail" onclick="showDetail(${item.id})">
<i class="fas fa-file-alt me-1"></i> Detail
</button>
</td>
@ -261,7 +304,7 @@ function showLoading() {
<tr>
<td colspan="7" class="empty-state-modern text-danger">
<i class="fas fa-exclamation-circle"></i>
Gagal memuat data riwayat.
${escapeHtml(error.message || 'Gagal memuat data riwayat.')}
</td>
</tr>
`;
@ -307,18 +350,26 @@ function renderPagination(data) {
let pages = [];
if (last <= 5) {
for (let i = 1; i <= last; i++) pages.push(i);
for (let i = 1; i <= last; i++) {
pages.push(i);
}
} else {
pages.push(1);
if (current > 3) pages.push('...');
if (current > 3) {
pages.push('...');
}
const start = Math.max(2, current - 1);
const end = Math.min(last - 1, current + 1);
for (let i = start; i <= end; i++) pages.push(i);
for (let i = start; i <= end; i++) {
pages.push(i);
}
if (current < last - 2) pages.push('...');
if (current < last - 2) {
pages.push('...');
}
pages.push(last);
}
@ -348,9 +399,14 @@ function renderObjectGrid(data, className) {
}
async function showDetail(id) {
const token = localStorage.getItem('access_token');
const token = getToken();
const modalBody = document.getElementById('modal-body-content');
if (!token) {
window.location.href = LOGIN_URL;
return;
}
const detailModal = new bootstrap.Modal(document.getElementById('detailModal'));
detailModal.show();
@ -362,76 +418,86 @@ function renderObjectGrid(data, className) {
`;
try {
const response = await fetch(`/api/admin/riwayat/${id}`, {
const response = await fetch(`${API_RIWAYAT_URL}/${encodeURIComponent(id)}`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/json'
}
});
const result = await response.json();
const result = await parseJsonResponse(response);
if (result.success) {
const data = result.data;
modalBody.innerHTML = `
<div class="detail-result-card">
<small>Hasil Rekomendasi Random Forest</small>
<h4>${escapeHtml(data.hasil_rekomendasi)}</h4>
</div>
<div class="detail-grid">
<div class="detail-info-card">
<span>Nama Siswa</span>
<strong>${escapeHtml(data.nama_siswa)}</strong>
</div>
<div class="detail-info-card">
<span>Email Siswa</span>
<strong>${escapeHtml(data.email_siswa)}</strong>
</div>
<div class="detail-info-card">
<span>Waktu Prediksi</span>
<strong>${escapeHtml(data.tanggal)}</strong>
</div>
<div class="detail-info-card">
<span>Tingkat Keyakinan</span>
<strong>${escapeHtml(formatPercent(data.akurasi_prediksi))}</strong>
</div>
<div class="detail-info-card">
<span>Minat Utama</span>
<strong>${escapeHtml(data.minat_utama)}</strong>
</div>
<div class="detail-info-card">
<span>Rata-rata Nilai</span>
<strong>${escapeHtml(data.rata_nilai)}</strong>
</div>
<div class="detail-info-card">
<span>Kondisi Ekonomi</span>
<strong>${escapeHtml(data.kondisi_ekonomi)} · ${escapeHtml(formatRupiah(data.gaji_ortu))}</strong>
</div>
</div>
<h6 class="detail-section-title">Nilai Rapor Ringkas</h6>
<div class="mini-data-grid">
${renderObjectGrid(data.nilai_ringkas, 'mini-data-item')}
</div>
<h6 class="detail-section-title">Skor Minat RIASEC</h6>
<div class="mini-data-grid">
${renderObjectGrid(data.riasec, 'mini-data-item')}
</div>
`;
} else {
modalBody.innerHTML = `<div class="alert alert-danger m-0">Gagal memuat detail: ${escapeHtml(result.message)}</div>`;
if (response.status === 401 || response.status === 403) {
window.location.href = LOGIN_URL;
return;
}
if (!response.ok || result.success === false) {
throw new Error(result.message || 'Gagal memuat detail riwayat.');
}
const data = result.data || {};
modalBody.innerHTML = `
<div class="detail-result-card">
<small>Hasil Rekomendasi Random Forest</small>
<h4>${escapeHtml(data.hasil_rekomendasi)}</h4>
</div>
<div class="detail-grid">
<div class="detail-info-card">
<span>Nama Siswa</span>
<strong>${escapeHtml(data.nama_siswa)}</strong>
</div>
<div class="detail-info-card">
<span>Email Siswa</span>
<strong>${escapeHtml(data.email_siswa)}</strong>
</div>
<div class="detail-info-card">
<span>Waktu Prediksi</span>
<strong>${escapeHtml(data.tanggal)}</strong>
</div>
<div class="detail-info-card">
<span>Tingkat Keyakinan</span>
<strong>${escapeHtml(formatPercent(data.akurasi_prediksi))}</strong>
</div>
<div class="detail-info-card">
<span>Minat Utama</span>
<strong>${escapeHtml(data.minat_utama)}</strong>
</div>
<div class="detail-info-card">
<span>Rata-rata Nilai</span>
<strong>${escapeHtml(data.rata_nilai)}</strong>
</div>
<div class="detail-info-card">
<span>Kondisi Ekonomi</span>
<strong>${escapeHtml(data.kondisi_ekonomi)} · ${escapeHtml(formatRupiah(data.gaji_ortu))}</strong>
</div>
</div>
<h6 class="detail-section-title">Nilai Rapor Ringkas</h6>
<div class="mini-data-grid">
${renderObjectGrid(data.nilai_ringkas, 'mini-data-item')}
</div>
<h6 class="detail-section-title">Skor Minat RIASEC</h6>
<div class="mini-data-grid">
${renderObjectGrid(data.riasec, 'mini-data-item')}
</div>
`;
} catch (error) {
modalBody.innerHTML = `<div class="alert alert-danger m-0">Terjadi kesalahan jaringan saat mengambil detail.</div>`;
modalBody.innerHTML = `
<div class="alert alert-danger m-0">
${escapeHtml(error.message || 'Terjadi kesalahan jaringan saat mengambil detail.')}
</div>
`;
}
}
@ -453,5 +519,4 @@ function renderObjectGrid(data, className) {
});
});
</script>
@endpush
@endsection

View File

@ -21,271 +21,187 @@
<div class="table-toolbar">
<div class="search-wrapper-modern">
<i class="fas fa-search"></i>
<input type="text" id="searchInput" class="search-input-modern" placeholder="Cari nama siswa, NISN, kelas, atau peminatan...">
<input type="text" id="searchInput" class="search-input-modern" placeholder="Cari nama siswa, NISN, atau kelas...">
</div>
<div class="per-page-wrapper">
<span>Tampilkan</span>
<select id="perPageSelect" class="per-page-select">
<option value="7" selected>7 data</option>
<option value="5">5 data</option>
<option value="7" selected>7 data</option>
<option value="10">10 data</option>
<option value="20">20 data</option>
</select>
</div>
</div>
<div class="table-responsive">
<table class="table table-modern mb-0">
<thead>
<tr>
<th class="text-center" width="70">No</th>
<th class="text-center" width="110">ID Siswa</th>
<th>Nama Lengkap</th>
<th>NISN</th>
<th class="text-center">Kelas</th>
<th class="text-center" width="150">Opsi</th>
</tr>
</thead>
<tbody id="siswa-table-body">
<tr>
<td colspan="6" class="loading-state-modern">
<i class="fas fa-spinner fa-spin me-2"></i> Memuat data siswa...
</td>
</tr>
</tbody>
</table>
</div>
<div class="pagination-container">
<div class="text-muted small" id="pagination-info">
Menampilkan 0 sampai 0 dari 0 data
<div id="tableContent">
</div>
<div class="d-flex gap-1 flex-wrap" id="pagination-controls"></div>
</div>
</div>
</div>
<script>
// Konfigurasi API Mandiri
const API_SISWA_URL = "https://ta.myhost.id/E31230935/api/admin/siswa";
const LOGIN_URL = "https://ta.myhost.id/E31230935/login";
function getToken() {
return localStorage.getItem('access_token');
}
async function parseJsonResponse(response) {
if (response.status === 401 || response.status === 403) {
Swal.fire('Sesi Berakhir', 'Silakan login ulang.', 'warning')
.then(() => window.location.href = LOGIN_URL);
return { success: false, message: 'Unauthorized' };
}
return await response.json();
}
let currentPage = 1;
let currentSearch = '';
let perPage = 7;
let searchTimer = null;
function escapeHtml(value) {
if (value === null || value === undefined || value === '') return '-';
return String(value)
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#039;');
}
function getInitial(name) {
const cleanName = name || 'S';
return cleanName.charAt(0).toUpperCase();
}
function showLoading() {
document.getElementById('siswa-table-body').innerHTML = `
<tr>
<td colspan="6" class="loading-state-modern">
<i class="fas fa-spinner fa-spin me-2"></i> Memuat data siswa...
</td>
</tr>
`;
}
let searchTimer;
async function fetchSiswa(page = 1) {
const token = localStorage.getItem('access_token');
const tbody = document.getElementById('siswa-table-body');
if (!token) {
window.location.href = '/login';
return;
}
currentPage = page;
showLoading();
const params = new URLSearchParams({
page: page,
per_page: perPage
});
if (currentSearch !== '') {
params.append('search', currentSearch);
}
const tableContent = document.getElementById('tableContent');
tableContent.innerHTML = '<div class="text-center py-5 text-muted"><span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span> Memuat data...</div>';
try {
const response = await fetch(`/api/admin/siswa?${params.toString()}`, {
method: 'GET',
const token = getToken();
const response = await fetch(`${API_SISWA_URL}?page=${page}&per_page=${perPage}&search=${encodeURIComponent(currentSearch)}`, {
headers: {
'Authorization': 'Bearer ' + token,
'Accept': 'application/json'
}
});
const data = await parseJsonResponse(response);
const result = await response.json();
if (!response.ok || result.success === false) {
throw new Error(result.message || 'Gagal memuat data siswa.');
if (response.ok && data.success !== false) {
renderTable(data.data.data, data.data.current_page, data.data.per_page);
renderPagination(data.data);
} else {
tableContent.innerHTML = `<div class="text-center py-5 text-danger"><i class="fas fa-exclamation-triangle fa-3x mb-3"></i><p>Gagal memuat data: ${data.message || 'Error server'}</p></div>`;
}
const data = result.data || [];
tbody.innerHTML = '';
if (data.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="6" class="empty-state-modern">
<i class="fas fa-folder-open"></i>
Tidak ada data siswa yang ditemukan.
</td>
</tr>
`;
updatePaginationInfo(null);
renderPagination({ current_page: 1, last_page: 1, total: 0 });
return;
}
data.forEach((item, index) => {
tbody.innerHTML += `
<tr>
<td class="text-center text-muted fw-bold">${(result.from || 1) + index}</td>
<td class="text-center fw-bold text-muted">${escapeHtml(item.id_siswa)}</td>
<td>
<div class="table-user-cell">
<div class="avatar-circle">${getInitial(item.nama_lengkap)}</div>
<div>
<div class="fw-bold text-dark">${escapeHtml(item.nama_lengkap)}</div>
<div class="text-cell-muted">Data siswa aktif</div>
</div>
</div>
</td>
<td class="text-muted fw-semibold">${escapeHtml(item.nisn)}</td>
<td class="text-center">
<span class="badge-soft badge-kelas">${escapeHtml(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>
`;
});
updatePaginationInfo(result);
renderPagination(result);
} catch (error) {
tbody.innerHTML = `
<tr>
<td colspan="6" class="empty-state-modern text-danger">
<i class="fas fa-exclamation-circle"></i>
Gagal memuat data siswa.
</td>
</tr>
`;
tableContent.innerHTML = `<div class="text-center py-5 text-danger"><i class="fas fa-wifi fa-3x mb-3"></i><p>Gagal menghubungi server. Periksa koneksi internet Anda.</p></div>`;
}
}
function updatePaginationInfo(data) {
const info = document.getElementById('pagination-info');
function renderTable(siswaData, currentPage, perPage) {
const tableContent = document.getElementById('tableContent');
if (!data || !data.total) {
info.innerText = 'Menampilkan 0 sampai 0 dari 0 data';
if (siswaData.length === 0) {
tableContent.innerHTML = '<div class="text-center py-5 text-muted"><i class="fas fa-folder-open fa-3x mb-3"></i><p>Tidak ada data siswa yang ditemukan.</p></div>';
return;
}
info.innerText = `Menampilkan ${data.from || 0} sampai ${data.to || 0} dari ${data.total || 0} data`;
}
let html = `
<div class="table-responsive">
<table class="table table-hover align-middle table-modern">
<thead>
<tr>
<th width="5%" class="text-center">No</th>
<th width="25%">Nama Lengkap</th>
<th width="20%">NISN</th>
<th width="15%" class="text-center">Kelas</th>
<th width="20%">Username</th>
<th width="15%" class="text-center">Aksi</th>
</tr>
</thead>
<tbody>
`;
function renderPagination(data) {
const controls = document.getElementById('pagination-controls');
controls.innerHTML = '';
siswaData.forEach((siswa, index) => {
const no = (currentPage - 1) * perPage + (index + 1);
const editUrl = `{{ url('/admin/siswa') }}/${siswa.id}/edit`;
const username = siswa.user ? siswa.user.username : '<span class="text-danger small">Tidak ada akun</span>';
const current = Number(data.current_page || 1);
const last = Number(data.last_page || 1);
const total = Number(data.total || 0);
if (total === 0) return;
const createButton = (label, page, disabled = false, active = false) => {
const btn = document.createElement('button');
btn.className = `page-link-custom ${active ? 'active' : ''}`;
btn.innerHTML = label;
btn.disabled = disabled;
btn.onclick = () => {
if (!disabled && !active) fetchSiswa(page);
};
return btn;
};
controls.appendChild(createButton('<i class="fas fa-chevron-left"></i>', current - 1, current === 1));
let pages = [];
if (last <= 5) {
for (let i = 1; i <= last; i++) pages.push(i);
} else {
pages.push(1);
if (current > 3) pages.push('...');
const start = Math.max(2, current - 1);
const end = Math.min(last - 1, current + 1);
for (let i = start; i <= end; i++) pages.push(i);
if (current < last - 2) pages.push('...');
pages.push(last);
}
pages.forEach(page => {
if (page === '...') {
controls.appendChild(createButton('...', current, true));
} else {
controls.appendChild(createButton(page, page, false, page === current));
}
html += `
<tr>
<td class="text-center">${no}</td>
<td class="fw-bold">${siswa.nama_lengkap || '-'}</td>
<td><span class="badge bg-light text-dark border">${siswa.nisn || '-'}</span></td>
<td class="text-center"><span class="badge" style="background-color: #2D7A47;">${siswa.kelas || '-'}</span></td>
<td>${username}</td>
<td class="text-center">
<div class="d-flex justify-content-center gap-2">
<a href="${editUrl}" class="btn btn-sm btn-outline-primary shadow-sm" title="Edit Data">
<i class="fas fa-edit"></i>
</a>
<button onclick="deleteSiswa(${siswa.id})" class="btn btn-sm btn-outline-danger shadow-sm" title="Hapus Data">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
`;
});
controls.appendChild(createButton('<i class="fas fa-chevron-right"></i>', current + 1, current === last));
html += `
</tbody>
</table>
</div>
<div id="paginationWrapper" class="d-flex justify-content-between align-items-center p-3 border-top bg-light mt-3 rounded-bottom"></div>
`;
tableContent.innerHTML = html;
}
function renderPagination(meta) {
const paginationWrapper = document.getElementById('paginationWrapper');
if (!paginationWrapper) return;
let html = `<div class="text-muted small fw-bold">Menampilkan ${meta.from || 0} - ${meta.to || 0} dari ${meta.total || 0} Data</div>`;
html += `<div class="d-flex gap-2">`;
if (meta.current_page > 1) {
html += `<button onclick="fetchSiswa(${meta.current_page - 1})" class="btn btn-sm btn-light border fw-bold text-muted shadow-sm"><i class="fas fa-chevron-left me-1"></i> Sebelumnya</button>`;
}
if (meta.current_page < meta.last_page) {
html += `<button onclick="fetchSiswa(${meta.current_page + 1})" class="btn btn-sm btn-light border fw-bold text-muted shadow-sm">Selanjutnya <i class="fas fa-chevron-right ms-1"></i></button>`;
}
html += `</div>`;
paginationWrapper.innerHTML = html;
}
async function deleteSiswa(id) {
const result = await Swal.fire({
const confirmResult = await Swal.fire({
title: 'Hapus data?',
text: 'Data siswa akan dihapus permanen!',
text: "Data yang dihapus tidak dapat dikembalikan!",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#ef4444',
cancelButtonColor: '#64748b',
confirmButtonText: 'Ya, Hapus',
confirmButtonColor: '#d33',
cancelButtonColor: '#3085d6',
confirmButtonText: 'Ya, Hapus!',
cancelButtonText: 'Batal'
});
if (!result.isConfirmed) return;
const token = localStorage.getItem('access_token');
if (!confirmResult.isConfirmed) return;
const token = getToken();
try {
const response = await fetch('/api/admin/siswa/' + id, {
const response = await fetch(`${API_SISWA_URL}/${id}`, {
method: 'DELETE',
headers: {
'Authorization': 'Bearer ' + token,
'Accept': 'application/json'
}
});
const resultDelete = await parseJsonResponse(response);
if (response.ok) {
Swal.fire('Berhasil!', 'Data siswa berhasil dihapus.', 'success');
if (response.ok && resultDelete.success !== false) {
Swal.fire('Terhapus!', resultDelete.message || 'Data berhasil dihapus.', 'success');
fetchSiswa(currentPage);
} else {
Swal.fire('Gagal!', 'Data siswa gagal dihapus.', 'error');
Swal.fire('Gagal!', resultDelete.message || 'Gagal menghapus data.', 'error');
}
} catch (error) {
Swal.fire('Error!', 'Koneksi ke server gagal.', 'error');
Swal.fire('Error!', 'Gagal menghubungi server.', 'error');
}
}
@ -297,11 +213,11 @@ function renderPagination(data) {
searchTimer = setTimeout(() => {
currentSearch = this.value.trim();
fetchSiswa(1);
}, 350);
}, 500);
});
document.getElementById('perPageSelect').addEventListener('change', function () {
perPage = this.value;
perPage = parseInt(this.value);
fetchSiswa(1);
});
});

View File

@ -7,52 +7,73 @@
<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>
<h4 class="fw-bold text-dark">Tambah Data Siswa Baru</h4>
<p class="text-muted small">Isi data di bawah ini untuk membuat profil siswa sekaligus akun login sistem.</p>
</div>
<div class="card border-0 shadow-sm rounded-4">
<div class="card-body p-5">
<form id="createSiswaForm">
<form id="formCreateSiswa">
<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 class="col-12">
<div class="text-muted small fw-bold text-uppercase mb-2" style="letter-spacing: 1px;">
<i class="fas fa-key me-2"></i>Informasi Akun Login
</div>
<hr class="mt-0 opacity-25">
</div>
<div class="col-md-6 form-group-custom">
<label class="fw-bold small text-muted">Username Login</label>
<input type="text" id="username" class="form-control input-custom" placeholder="Contoh: siswabaru12" required>
</div>
<div class="col-md-6 form-group-custom">
<label class="fw-bold small text-muted">Password</label>
<input type="password" id="password" class="form-control input-custom" placeholder="Minimal 6 karakter" required>
</div>
<div class="col-12 mt-4">
<div class="text-muted small fw-bold text-uppercase mb-2" style="letter-spacing: 1px;">
<i class="fas fa-user me-2"></i>Data Profil Siswa
</div>
<hr class="mt-0 opacity-25">
</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">
<label class="fw-bold small text-muted">Nama Lengkap</label>
<input type="text" id="nama_lengkap" class="form-control input-custom" placeholder="Masukkan nama sesuai ijazah/rapor" required>
</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 class="col-md-6 form-group-custom">
<label class="fw-bold small text-muted">NISN</label>
<input type="text" id="nisn" class="form-control input-custom" placeholder="10 Digit nomor NISN" maxlength="10" required>
</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>
<div class="col-md-6 form-group-custom">
<label class="fw-bold small text-muted">Kelas</label>
<select id="kelas" class="form-select input-custom" required>
<option value="" disabled selected>Pilih Kelas...</option>
<option value="XII MIPA 1">XII MIPA 1</option>
<option value="XII MIPA 2">XII MIPA 2</option>
<option value="XII MIPA 3">XII MIPA 3</option>
<option value="XII MIPA 4">XII MIPA 4</option>
<option value="XII IPS 1">XII IPS 1</option>
<option value="XII IPS 2">XII IPS 2</option>
<option value="XII IPS 3">XII IPS 3</option>
<option value="XII IPS 4">XII IPS 4</option>
<option value="XII BAHASA">XII BAHASA</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>
<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>
@ -61,43 +82,109 @@
</div>
<script>
document.getElementById('createSiswaForm').addEventListener('submit', async (e) => {
function getAppBaseUrl() {
const origin = window.location.origin;
const segments = window.location.pathname.split('/').filter(Boolean);
const adminIndex = segments.indexOf('admin');
if (adminIndex > 0) {
return origin + '/' + segments.slice(0, adminIndex).join('/');
}
return origin;
}
const APP_BASE_URL = getAppBaseUrl();
const API_SISWA_URL = `${APP_BASE_URL}/api/admin/siswa`;
const LOGIN_URL = `${APP_BASE_URL}/login`;
const SISWA_INDEX_URL = `${APP_BASE_URL}/admin/siswa`;
function getToken() {
return localStorage.getItem('access_token') || localStorage.getItem('token') || '';
}
function getValue(id) {
return document.getElementById(id).value.trim();
}
async function parseJsonResponse(response) {
const text = await response.text();
try {
return text ? JSON.parse(text) : {};
} catch (error) {
return {
success: false,
message: 'Respon server tidak valid.'
};
}
}
function formatErrors(errors) {
if (!errors) return '';
return Object.values(errors).flat().map(item => String(item)).join('<br>');
}
document.getElementById('formCreateSiswa').addEventListener('submit', async function (e) {
e.preventDefault();
const token = getToken();
if (!token) {
Swal.fire('Sesi Berakhir', 'Silakan login ulang.', 'warning')
.then(() => window.location.href = LOGIN_URL);
return;
}
const btn = document.getElementById('btnSave');
btn.disabled = true;
btn.innerText = 'Menyimpan...';
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,
username: getValue('username'),
password: getValue('password'),
nama_lengkap: getValue('nama_lengkap'),
nisn: getValue('nisn'),
kelas: getValue('kelas')
};
try {
const response = await fetch('/api/admin/siswa', {
const response = await fetch(API_SISWA_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('access_token'),
'Authorization': 'Bearer ' + token,
'Accept': 'application/json'
},
body: JSON.stringify(payload)
});
const result = await response.json();
const result = await parseJsonResponse(response);
if (response.ok) {
Swal.fire({ icon: 'success', title: 'Berhasil!', text: 'Data siswa ditambahkan.', confirmButtonColor: '#2D7A47' })
.then(() => { window.location.href = "{{ route('admin.siswa') }}"; });
if (response.status === 401 || response.status === 403) {
Swal.fire('Sesi Berakhir', 'Silakan login ulang.', 'warning')
.then(() => window.location.href = LOGIN_URL);
return;
}
if (response.ok && result.success !== false) {
Swal.fire({
icon: 'success',
title: 'Berhasil!',
text: result.message || 'Data siswa berhasil ditambahkan.',
confirmButtonColor: '#2D6A4F'
}).then(() => {
window.location.href = SISWA_INDEX_URL;
});
} else {
Swal.fire('Gagal!', result.message || 'Cek kembali data Anda.', 'warning');
Swal.fire({
icon: 'warning',
title: 'Gagal!',
html: formatErrors(result.errors) || result.message || 'Periksa kembali isian Anda.',
confirmButtonColor: '#2D6A4F'
});
}
} catch (error) {
Swal.fire('Error!', 'Gagal menghubungi server.', 'error');
Swal.fire('Error!', error.message || 'Gagal menghubungi server.', 'error');
} finally {
btn.disabled = false;
btn.innerText = 'Simpan Data';
}
});
</script>

View File

@ -7,43 +7,76 @@
<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>
<h4 class="fw-bold text-dark">Edit Data Profil Siswa</h4>
<p class="text-muted small">Ubah rincian informasi dan akun profil siswa.</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 }}">
<form id="formEditSiswa">
<input type="hidden" id="siswa_id" value="{{ $id }}">
<div class="row g-4">
<div class="col-12">
<div class="text-muted small fw-bold text-uppercase mb-2" style="letter-spacing: 1px;">
<i class="fas fa-key me-2"></i>Informasi Akun Login
</div>
<hr class="mt-0 opacity-25">
</div>
<div class="col-md-6 form-group-custom">
<label class="fw-bold small text-muted">Username Login (Opsional)</label>
<input type="text" id="username" class="form-control input-custom" placeholder="Kosongkan jika tidak diubah">
</div>
<div class="col-md-6 form-group-custom">
<label class="fw-bold small text-muted">Password Baru (Opsional)</label>
<input type="password" id="password" class="form-control input-custom" placeholder="Kosongkan jika tidak diubah">
</div>
<div class="col-12 mt-4">
<div class="text-muted small fw-bold text-uppercase mb-2" style="letter-spacing: 1px;">
<i class="fas fa-user me-2"></i>Data Profil Siswa
</div>
<hr class="mt-0 opacity-25">
</div>
<div class="col-md-12 form-group-custom">
<label>Nama Lengkap</label>
<label class="fw-bold small text-muted">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 class="col-md-6 form-group-custom">
<label class="fw-bold small text-muted">NISN</label>
<input type="text" id="nisn" class="form-control input-custom" maxlength="10" 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>
<div class="col-md-6 form-group-custom">
<label class="fw-bold small text-muted">Kelas</label>
<select id="kelas" class="form-select input-custom" required>
<option value="" disabled selected>Pilih Kelas...</option>
<option value="XII MIPA 1">XII MIPA 1</option>
<option value="XII MIPA 2">XII MIPA 2</option>
<option value="XII MIPA 3">XII MIPA 3</option>
<option value="XII MIPA 4">XII MIPA 4</option>
<option value="XII IPS 1">XII IPS 1</option>
<option value="XII IPS 2">XII IPS 2</option>
<option value="XII IPS 3">XII IPS 3</option>
<option value="XII IPS 4">XII IPS 4</option>
<option value="XII BAHASA">XII BAHASA</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>
<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>
@ -52,71 +85,134 @@
</div>
<script>
const dbId = document.getElementById('siswa_id_db').value;
function getAppBaseUrl() {
const origin = window.location.origin;
const segments = window.location.pathname.split('/').filter(Boolean);
const adminIndex = segments.indexOf('admin');
if (adminIndex > 0) {
return origin + '/' + segments.slice(0, adminIndex).join('/');
}
return origin;
}
// Mengambil data lama untuk ditampilkan di form
async function loadSiswa() {
const APP_BASE_URL = getAppBaseUrl();
const API_SISWA_URL = `${APP_BASE_URL}/api/admin/siswa`;
const LOGIN_URL = `${APP_BASE_URL}/login`;
const SISWA_INDEX_URL = `${APP_BASE_URL}/admin/siswa`;
function getToken() {
return localStorage.getItem('access_token') || localStorage.getItem('token') || '';
}
async function parseJsonResponse(response) {
const text = await response.text();
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');
}
return text ? JSON.parse(text) : {};
} catch (error) {
Swal.fire('Error!', 'Gagal mengambil data dari server.', 'error');
return { success: false, message: 'Respon server tidak valid.' };
}
}
// Mengirim data perbaikan ke API
document.getElementById('editSiswaForm').addEventListener('submit', async (e) => {
e.preventDefault();
const btn = document.getElementById('btnUpdate');
btn.disabled = true;
document.addEventListener('DOMContentLoaded', async function () {
const id = document.getElementById('siswa_id').value;
const token = getToken();
const payload = {
nama_lengkap: document.getElementById('nama_lengkap').value,
nisn: document.getElementById('nisn').value,
kelas: document.getElementById('kelas').value,
peminatan: document.getElementById('peminatan').value
};
if (!token) {
window.location.href = LOGIN_URL;
return;
}
try {
const response = await fetch(`/api/admin/siswa/${dbId}`, {
const response = await fetch(`${API_SISWA_URL}/${id}`, {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + token,
'Accept': 'application/json'
}
});
const result = await parseJsonResponse(response);
if (response.ok && result.success) {
const s = result.data;
document.getElementById('nama_lengkap').value = s.nama_lengkap || '';
document.getElementById('nisn').value = s.nisn || '';
if (s.kelas) {
document.getElementById('kelas').value = s.kelas;
}
if (s.user && s.user.username) {
document.getElementById('username').placeholder = `Username saat ini: ${s.user.username}`;
}
} else {
Swal.fire('Gagal!', result.message || 'Gagal memuat rincian data siswa.', 'error');
}
} catch (error) {
Swal.fire('Error!', 'Gagal menghubungi server untuk memuat data.', 'error');
}
});
document.getElementById('formEditSiswa').addEventListener('submit', async function (e) {
e.preventDefault();
const id = document.getElementById('siswa_id').value;
const btn = document.getElementById('btnUpdate');
btn.disabled = true;
btn.innerText = 'Memperbarui...';
const payload = {
nama_lengkap: document.getElementById('nama_lengkap').value.trim(),
nisn: document.getElementById('nisn').value.trim(),
kelas: document.getElementById('kelas').value
};
const passwordInput = document.getElementById('password').value.trim();
if (passwordInput.length > 0) {
payload.password = passwordInput;
}
const usernameInput = document.getElementById('username').value.trim();
if (usernameInput.length > 0) {
payload.username = usernameInput;
}
try {
const response = await fetch(`${API_SISWA_URL}/${id}`, {
method: 'PUT',
headers: {
'Authorization': 'Bearer ' + getToken(),
'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') }}"; });
const result = await parseJsonResponse(response);
if (response.ok && result.success !== false) {
Swal.fire({
icon: 'success',
title: 'Berhasil Diperbarui!',
text: result.message || 'Data siswa berhasil diperbarui.',
confirmButtonColor: '#2D7A47'
}).then(() => {
window.location.href = SISWA_INDEX_URL;
});
} else {
const result = await response.json();
Swal.fire('Gagal!', result.message || 'Cek inputan.', 'warning');
Swal.fire({
icon: 'warning',
title: 'Gagal!',
text: result.message || 'Cek kembali rincian data yang Anda ubah.',
confirmButtonColor: '#2D7A47'
});
}
} catch (error) {
Swal.fire('Error!', 'Kesalahan koneksi server.', 'error');
Swal.fire('Error!', 'Gagal menghubungi server.', 'error');
} finally {
btn.disabled = false;
btn.innerText = 'Perbarui Data';
}
});
// Jalankan load data saat halaman siap
document.addEventListener('DOMContentLoaded', loadSiswa);
</script>
@endsection

View File

@ -75,20 +75,26 @@ class="input-modern w-full text-slate-900"
</div>
<script>
const apiLoginUrl = @json(route('api.login'));
const adminDashboardUrl = @json(route('admin.dashboard'));
const studentDashboardUrl = @json(route('student.profile'));
document.getElementById('loginForm').addEventListener('submit', async function(e) {
e.preventDefault();
const btn = document.getElementById('btnLogin');
const username = document.getElementById('username').value;
const username = document.getElementById('username').value.trim();
const password = document.getElementById('password').value;
const errorElement = document.getElementById('error-message');
errorElement.classList.add('hidden');
errorElement.innerText = '';
btn.disabled = true;
btn.innerText = 'Memproses...';
try {
const response = await fetch('/api/login', {
const response = await fetch(apiLoginUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -100,16 +106,32 @@ class="input-modern w-full text-slate-900"
})
});
const data = await response.json();
const contentType = response.headers.get('content-type') || '';
let data = {};
if (response.ok) {
if (contentType.includes('application/json')) {
data = await response.json();
} else {
const text = await response.text();
console.error('Response bukan JSON:', text);
throw new Error('Server tidak mengembalikan JSON.');
}
if (response.ok && data.success) {
localStorage.setItem('access_token', data.access_token);
localStorage.setItem('user_role', data.role);
if (data.user) {
localStorage.setItem('user_name', data.user.name || data.user.username || 'User');
}
if (data.role === 'admin') {
window.location.href = "{{ route('admin.dashboard') }}";
window.location.href = adminDashboardUrl;
} else if (data.role === 'siswa') {
window.location.href = studentDashboardUrl;
} else {
window.location.href = "{{ route('student.profile') }}";
errorElement.innerText = 'Role pengguna tidak dikenali.';
errorElement.classList.remove('hidden');
}
} else {
errorElement.innerText = data.message || 'Username atau password salah.';

View File

@ -107,13 +107,13 @@
<body class="flex flex-col min-h-screen text-slate-800">
<!-- Navbar -->
<nav class="sticky top-0 z-50 bg-white/95 backdrop-blur-md border-b border-slate-100 px-6 py-4">
<div class="max-w-7xl mx-auto flex justify-between items-center gap-4">
<div class="flex items-center gap-4">
<div class="w-10 h-10 bg-emerald-100 rounded-xl flex items-center justify-center text-emerald-600 font-bold">
AS
</div>
<span class="font-black text-xl tracking-tighter text-emerald-950 uppercase italic">
ASGARD.
</span>
@ -129,19 +129,15 @@
Cek Rekomendasi
</a>
<!-- Menu ini membuka hasil rekomendasi terakhir -->
<a href="#" onclick="goToLastResult(event)" class="hover:text-emerald-600 transition">
Hasil Rekomendasi
</a>
<!-- <a href="#" class="hover:text-emerald-600 transition">
Kontak BK
</a> -->
</div>
<div class="flex items-center gap-3">
<div class="hidden sm:flex items-center gap-3 bg-emerald-50 px-4 py-2 rounded-full border border-emerald-100">
<div class="w-2 h-2 bg-emerald-500 rounded-full animate-pulse"></div>
<span id="student-name-label" class="text-[10px] font-bold text-emerald-700 uppercase">
Siswa
</span>
@ -159,7 +155,6 @@
<main class="max-w-7xl mx-auto px-6 flex-grow py-10">
<!-- Header -->
<header class="mb-10">
<h1 class="text-4xl font-extrabold text-slate-900 tracking-tight">
Halo, Siswa SMAPA! 👋
@ -170,7 +165,6 @@
</p>
</header>
<!-- Statistik -->
<section class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-10">
<div class="bg-white p-8 rounded-3xl card-shadow border border-slate-100 card-hover">
<h4 class="text-slate-400 text-xs font-bold uppercase tracking-widest mb-4">
@ -213,7 +207,6 @@
</div>
</section>
<!-- Akses cepat -->
<section class="bg-white p-8 md:p-10 rounded-[2.5rem] card-shadow border border-slate-100 mb-10">
<div class="mb-8">
<h3 class="text-xl font-black text-slate-900">
@ -236,6 +229,7 @@ class="flex items-center p-6 bg-blue-50 rounded-2xl border border-blue-100 card-
<h4 class="font-black text-blue-900 text-sm">
Cek Rekomendasi Baru
</h4>
<p class="text-[11px] text-blue-700 font-semibold mt-1">
Isi data kembali untuk mendapatkan hasil terbaru.
</p>
@ -252,6 +246,7 @@ class="flex items-center p-6 bg-emerald-50 rounded-2xl border border-emerald-100
<h4 class="font-black text-emerald-900 text-sm">
Hasil Rekomendasi
</h4>
<p class="text-[11px] text-emerald-700 font-semibold mt-1">
Buka kembali hasil rekomendasi terakhir.
</p>
@ -268,6 +263,7 @@ class="flex items-center p-6 bg-violet-50 rounded-2xl border border-violet-100 c
<h4 class="font-black text-violet-900 text-sm">
Riwayat Rekomendasi
</h4>
<p class="text-[11px] text-violet-700 font-semibold mt-1">
Lihat daftar rekomendasi yang pernah dibuat.
</p>
@ -276,7 +272,6 @@ class="flex items-center p-6 bg-violet-50 rounded-2xl border border-violet-100 c
</div>
</section>
<!-- Grafik -->
<section class="bg-white p-8 md:p-10 rounded-[2.5rem] card-shadow border border-slate-100 mb-10">
<h3 class="text-xl font-black text-slate-900 mb-2">
Perkembangan Rata-Rata Nilai
@ -295,7 +290,6 @@ class="flex items-center p-6 bg-violet-50 rounded-2xl border border-violet-100 c
</p>
</section>
<!-- Panduan -->
<section id="panduan-section" class="bg-white p-8 md:p-10 rounded-[2.5rem] card-shadow border border-slate-100 mb-10">
<div class="flex flex-col lg:flex-row gap-8">
<div class="lg:w-1/3">
@ -346,7 +340,6 @@ class="flex items-center p-6 bg-violet-50 rounded-2xl border border-violet-100 c
</div>
</section>
<!-- Riwayat -->
<section id="riwayat-section" class="bg-white p-8 md:p-10 rounded-[2.5rem] card-shadow border border-slate-100 mb-16">
<div class="mb-6">
<h3 class="text-xl font-black text-slate-900">
@ -366,7 +359,6 @@ class="flex items-center p-6 bg-violet-50 rounded-2xl border border-violet-100 c
</section>
</main>
<!-- Footer -->
<footer class="bg-[#062C22] text-white py-16 px-6">
<div class="max-w-7xl mx-auto grid grid-cols-1 md:grid-cols-3 gap-12">
<div class="space-y-6">
@ -390,11 +382,6 @@ class="flex items-center p-6 bg-violet-50 rounded-2xl border border-violet-100 c
Panduan Sistem
</a>
</li>
<!-- <li>
<a href="#" class="hover:text-white transition">
Kebijakan Privasi
</a>
</li> -->
</ul>
</div>
@ -421,17 +408,57 @@ class="flex items-center p-6 bg-violet-50 rounded-2xl border border-violet-100 c
<script>
let trendChartInstance = null;
const routeLogin = @json(route('login'));
const routeDashboard = @json(route('student.profile'));
const routePrediction = @json(route('student.prediction'));
const routeAdminDashboard = @json(route('admin.dashboard'));
function getAppBaseUrl() {
const origin = window.location.origin;
const segments = window.location.pathname.split('/').filter(Boolean);
const studentIndex = segments.indexOf('student');
if (studentIndex > 0) {
return origin + '/' + segments.slice(0, studentIndex).join('/');
}
return origin;
}
const APP_BASE_URL = getAppBaseUrl();
const routeLogin = `${APP_BASE_URL}/login`;
const routeDashboard = `${APP_BASE_URL}/student/profile`;
const routePrediction = `${APP_BASE_URL}/student/prediction`;
const routeAdminDashboard = `${APP_BASE_URL}/admin/dashboard`;
const apiLogoutUrl = `${APP_BASE_URL}/api/logout`;
const apiStudentDashboardStatsUrl = `${APP_BASE_URL}/api/student/dashboard-stats`;
const studentResultUrlTemplate = `${APP_BASE_URL}/student/result/__ID__`;
function getAccessToken() {
return localStorage.getItem('access_token');
return localStorage.getItem('access_token') ||
localStorage.getItem('token') ||
sessionStorage.getItem('access_token') ||
sessionStorage.getItem('token') ||
'';
}
function getUserRole() {
return localStorage.getItem('user_role');
return localStorage.getItem('user_role') ||
localStorage.getItem('role') ||
sessionStorage.getItem('user_role') ||
sessionStorage.getItem('role') ||
'';
}
function clearLoginStorage() {
localStorage.removeItem('access_token');
localStorage.removeItem('token');
localStorage.removeItem('user_role');
localStorage.removeItem('role');
localStorage.removeItem('user_name');
localStorage.removeItem('name');
sessionStorage.removeItem('access_token');
sessionStorage.removeItem('token');
sessionStorage.removeItem('user_role');
sessionStorage.removeItem('role');
}
function redirectByRole(role) {
@ -440,7 +467,7 @@ function redirectByRole(role) {
return;
}
if (role === 'siswa') {
if (role === 'siswa' || role === 'student') {
window.location.href = routeDashboard;
return;
}
@ -448,6 +475,37 @@ function redirectByRole(role) {
window.location.href = routeLogin;
}
async function parseJsonResponse(response) {
const text = await response.text();
try {
return text ? JSON.parse(text) : {};
} catch (error) {
return {
success: false,
message: text && text.trim().startsWith('<')
? 'Server mengembalikan halaman HTML, bukan JSON. Biasanya URL API salah atau route API belum terdaftar.'
: (text || response.statusText || 'Respon server tidak valid.')
};
}
}
function normalizeAppUrl(url) {
if (!url) return '';
const text = String(url);
if (text.startsWith('http://') || text.startsWith('https://')) {
return text;
}
if (text.startsWith('/')) {
return APP_BASE_URL + text;
}
return `${APP_BASE_URL}/${text}`;
}
function getStoredLastResult() {
try {
const saved = localStorage.getItem('asgard_last_result');
@ -466,7 +524,7 @@ function saveStoredLastResult(data) {
id: data.id || oldData.id || '',
jurusan: data.jurusan || oldData.jurusan || '',
tanggal: data.tanggal || oldData.tanggal || '',
url: data.url || oldData.url || ''
url: normalizeAppUrl(data.url || oldData.url || '')
};
localStorage.setItem('asgard_last_result', JSON.stringify(nextData));
@ -478,12 +536,12 @@ function goToLastResult(event) {
const saved = getStoredLastResult();
if (saved && saved.url) {
window.location.href = saved.url;
window.location.href = normalizeAppUrl(saved.url);
return;
}
if (saved && saved.id) {
window.location.href = `/student/result/${saved.id}`;
window.location.href = makeResultUrl(saved.id);
return;
}
@ -504,7 +562,7 @@ function getHistoryId(item) {
}
function makeResultUrl(id) {
return id ? `/student/result/${id}` : '';
return id ? studentResultUrlTemplate.replace('__ID__', encodeURIComponent(id)) : '';
}
function isValidRecommendationName(value) {
@ -524,7 +582,7 @@ function isValidRecommendationName(value) {
if (token) {
try {
await fetch('/api/logout', {
await fetch(apiLogoutUrl, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
@ -536,8 +594,7 @@ function isValidRecommendationName(value) {
}
}
localStorage.removeItem('access_token');
localStorage.removeItem('user_role');
clearLoginStorage();
localStorage.removeItem('asgard_last_result');
window.location.href = routeLogin;
}
@ -691,7 +748,7 @@ function renderTrendChart(tren) {
return;
}
if (role !== 'siswa') {
if (role !== 'siswa' && role !== 'student') {
redirectByRole(role);
return;
}
@ -700,28 +757,24 @@ function renderTrendChart(tren) {
document.getElementById('student-name-label').innerText = savedName;
try {
const response = await fetch('/api/student/dashboard-stats', {
const response = await fetch(apiStudentDashboardStatsUrl, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/json'
}
});
if (response.status === 401) {
localStorage.removeItem('access_token');
localStorage.removeItem('user_role');
const result = await parseJsonResponse(response);
if (response.status === 401 || response.status === 403) {
clearLoginStorage();
window.location.href = routeLogin;
return;
}
const result = await response.json();
if (!result.success) {
document.getElementById('stat-total').innerText = 0;
document.getElementById('stat-last').innerText = 'Belum ada rekomendasi';
renderActivityList([]);
renderTrendChart([]);
return;
if (!response.ok || result.success === false) {
throw new Error(result.message || 'Gagal memuat data dashboard.');
}
const data = result.data || {};
@ -736,7 +789,6 @@ function renderTrendChart(tren) {
? lastName
: 'Belum ada rekomendasi';
// Simpan hasil terakhir dari data dashboard jika API mengirim id riwayat
const firstId = getHistoryId(firstActivity);
if (isValidRecommendationName(lastName) && firstId) {

View File

@ -100,13 +100,13 @@
<body class="flex flex-col min-h-screen text-slate-800">
<!-- Navbar -->
<nav class="sticky top-0 z-50 bg-white/95 backdrop-blur-md border-b border-slate-100 px-6 py-4">
<div class="max-w-7xl mx-auto flex items-center justify-between gap-4">
<div class="flex items-center gap-4">
<div class="w-10 h-10 bg-emerald-100 rounded-xl flex items-center justify-center text-emerald-600 font-bold">
AS
</div>
<span class="font-black text-xl tracking-tighter text-emerald-950 uppercase italic">
ASGARD.
</span>
@ -121,19 +121,15 @@
Cek Rekomendasi
</a>
<!-- Menu aktif halaman hasil -->
<a href="#" onclick="goToLastResult(event)" class="text-emerald-600">
Hasil Rekomendasi
</a>
<!-- <a href="#" class="hover:text-emerald-600 transition">
Kontak BK
</a> -->
</div>
<div class="flex items-center gap-3">
<div class="hidden sm:flex items-center gap-3 bg-emerald-50 px-4 py-2 rounded-full border border-emerald-100">
<div class="w-2 h-2 bg-emerald-500 rounded-full animate-pulse"></div>
<span class="text-[10px] font-bold text-emerald-700 uppercase">
Siswa
</span>
@ -151,7 +147,6 @@
<main class="flex-grow px-6 py-10">
<div class="max-w-5xl mx-auto">
<!-- Header -->
<div class="text-center mb-8">
<div class="inline-flex items-center gap-2 bg-emerald-100 text-emerald-700 px-4 py-2 rounded-full text-xs font-bold">
Hasil rekomendasi berhasil diproses
@ -166,7 +161,6 @@
</p>
</div>
<!-- Hasil utama -->
<section class="bg-gradient-to-br from-emerald-600 via-emerald-500 to-teal-600 rounded-[2.5rem] p-8 md:p-14 text-white card-shadow mb-8 relative overflow-hidden">
<div class="absolute inset-0 opacity-10">
<div class="absolute -top-10 -left-10 w-40 h-40 bg-white rounded-full blur-3xl"></div>
@ -187,7 +181,6 @@
</p>
<div class="flex flex-col sm:flex-row gap-3 justify-center">
<!-- Tombol kembali, hasil tetap tersimpan di localStorage -->
<a href="{{ route('student.profile') }}"
onclick="saveLastRecommendationResult()"
class="inline-flex items-center justify-center bg-white hover:bg-slate-100 text-emerald-700 font-bold py-4 px-8 rounded-2xl transition">
@ -203,7 +196,6 @@ class="inline-flex items-center justify-center bg-emerald-800/50 hover:bg-emeral
</div>
</section>
<!-- Rekapan data -->
<section class="bg-white rounded-[2rem] border border-slate-100 card-shadow p-6 md:p-8 mb-8">
<div class="flex items-center gap-3 mb-6">
<div class="w-10 h-10 rounded-2xl bg-violet-100 flex items-center justify-center text-xl">
@ -272,7 +264,6 @@ class="inline-flex items-center justify-center bg-emerald-800/50 hover:bg-emeral
</div>
</section>
<!-- Informasi hasil -->
<section class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
<div class="bg-white rounded-[2rem] border border-slate-100 card-shadow p-6 md:p-8">
<h3 class="text-xl font-extrabold text-slate-800 mb-4">
@ -291,13 +282,12 @@ class="inline-flex items-center justify-center bg-emerald-800/50 hover:bg-emeral
Peluang Karier
</h3>
<p class="text-slate-600 leading-relaxed">
{{ $prospekKerja }}
</p>
<div class="text-slate-600 leading-relaxed">
{!! $prospekKerja !!}
</div>
</div>
</section>
<!-- Saran -->
<section class="bg-white rounded-[2rem] border border-slate-100 card-shadow p-6 md:p-8">
<h3 class="text-xl font-extrabold text-slate-800 mb-5">
Langkah Setelah Mendapatkan Hasil
@ -338,7 +328,6 @@ class="inline-flex items-center justify-center bg-emerald-800/50 hover:bg-emeral
</div>
</main>
<!-- Footer -->
<footer class="bg-[#062C22] text-white py-16 px-6 mt-16">
<div class="max-w-7xl mx-auto grid grid-cols-1 md:grid-cols-3 gap-12">
<div class="space-y-6">
@ -362,11 +351,6 @@ class="inline-flex items-center justify-center bg-emerald-800/50 hover:bg-emeral
Panduan Sistem
</a>
</li>
<!-- <li>
<a href="#" class="hover:text-white transition">
Kebijakan Privasi
</a>
</li> -->
</ul>
</div>
@ -391,17 +375,55 @@ class="inline-flex items-center justify-center bg-emerald-800/50 hover:bg-emeral
</footer>
<script>
const routeLogin = @json(route('login'));
const routeDashboard = @json(route('student.profile'));
const routePrediction = @json(route('student.prediction'));
const routeAdminDashboard = @json(route('admin.dashboard'));
function getAppBaseUrl() {
const origin = window.location.origin;
const segments = window.location.pathname.split('/').filter(Boolean);
const studentIndex = segments.indexOf('student');
if (studentIndex > 0) {
return origin + '/' + segments.slice(0, studentIndex).join('/');
}
return origin;
}
const APP_BASE_URL = getAppBaseUrl();
const routeLogin = `${APP_BASE_URL}/login`;
const routeDashboard = `${APP_BASE_URL}/student/profile`;
const routePrediction = `${APP_BASE_URL}/student/prediction`;
const routeAdminDashboard = `${APP_BASE_URL}/admin/dashboard`;
const apiLogoutUrl = `${APP_BASE_URL}/api/logout`;
const studentResultUrlTemplate = `${APP_BASE_URL}/student/result/__ID__`;
function getAccessToken() {
return localStorage.getItem('access_token');
return localStorage.getItem('access_token') ||
localStorage.getItem('token') ||
sessionStorage.getItem('access_token') ||
sessionStorage.getItem('token') ||
'';
}
function getUserRole() {
return localStorage.getItem('user_role');
return localStorage.getItem('user_role') ||
localStorage.getItem('role') ||
sessionStorage.getItem('user_role') ||
sessionStorage.getItem('role') ||
'';
}
function clearLoginStorage() {
localStorage.removeItem('access_token');
localStorage.removeItem('token');
localStorage.removeItem('user_role');
localStorage.removeItem('role');
localStorage.removeItem('user_name');
localStorage.removeItem('name');
sessionStorage.removeItem('access_token');
sessionStorage.removeItem('token');
sessionStorage.removeItem('user_role');
sessionStorage.removeItem('role');
}
function redirectByRole(role) {
@ -410,7 +432,7 @@ function redirectByRole(role) {
return;
}
if (role === 'siswa') {
if (role === 'siswa' || role === 'student') {
window.location.href = routeDashboard;
return;
}
@ -418,6 +440,26 @@ function redirectByRole(role) {
window.location.href = routeLogin;
}
function makeResultUrl(id) {
return id ? studentResultUrlTemplate.replace('__ID__', encodeURIComponent(id)) : '';
}
function normalizeAppUrl(url) {
if (!url) return '';
const text = String(url);
if (text.startsWith('http://') || text.startsWith('https://')) {
return text;
}
if (text.startsWith('/')) {
return APP_BASE_URL + text;
}
return `${APP_BASE_URL}/${text}`;
}
function getStoredLastResult() {
try {
const saved = localStorage.getItem('asgard_last_result');
@ -435,12 +477,12 @@ function goToLastResult(event) {
const saved = getStoredLastResult();
if (saved && saved.url) {
window.location.href = saved.url;
window.location.href = normalizeAppUrl(saved.url);
return;
}
if (saved && saved.id) {
window.location.href = `/student/result/${saved.id}`;
window.location.href = makeResultUrl(saved.id);
return;
}
@ -448,10 +490,9 @@ function goToLastResult(event) {
window.location.href = routePrediction;
}
// Bagian penting: menyimpan hasil terakhir agar setelah klik kembali tetap bisa dibuka dari navbar
function saveLastRecommendationResult() {
const resultId = @json($riwayat->id ?? '');
const resultUrl = resultId ? `/student/result/${resultId}` : window.location.pathname;
const resultUrl = resultId ? makeResultUrl(resultId) : window.location.href;
const resultData = {
id: resultId,
@ -477,7 +518,7 @@ function saveLastRecommendationResult() {
if (token) {
try {
await fetch('/api/logout', {
await fetch(apiLogoutUrl, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
@ -489,13 +530,11 @@ function saveLastRecommendationResult() {
}
}
localStorage.removeItem('access_token');
localStorage.removeItem('user_role');
clearLoginStorage();
localStorage.removeItem('asgard_last_result');
window.location.href = routeLogin;
}
// bfcache/pageshow membuat hasil tetap tersimpan walaupun user memakai tombol back browser
window.addEventListener('pageshow', function () {
saveLastRecommendationResult();
});
@ -509,7 +548,7 @@ function saveLastRecommendationResult() {
return;
}
if (role !== 'siswa') {
if (role !== 'siswa' && role !== 'student') {
redirectByRole(role);
return;
}

View File

@ -6,98 +6,34 @@
<title>Cek Rekomendasi - ASGARD</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<meta name="csrf-token" content="{{ csrf_token() }}">
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Plus Jakarta Sans', sans-serif;
background:
radial-gradient(circle at 8% 12%, rgba(16, 185, 129, 0.10), transparent 32%),
radial-gradient(circle at 92% 8%, rgba(59, 130, 246, 0.08), transparent 30%),
linear-gradient(180deg, #f8fafc 0%, #f3f7fb 50%, #f8fafc 100%);
background: linear-gradient(180deg, #f8fafc 0%, #f3f7fb 50%, #f8fafc 100%);
}
nav {
box-shadow: 0 10px 35px rgba(15, 23, 42, 0.05);
}
nav a {
position: relative;
}
nav a::after {
content: "";
position: absolute;
left: 0;
bottom: -10px;
width: 0;
height: 2px;
border-radius: 999px;
background: #10b981;
transition: width 0.25s ease;
}
nav a:hover::after,
nav a.text-emerald-600::after {
width: 100%;
}
.card-shadow {
box-shadow: 0 18px 50px rgba(15, 23, 42, 0.08);
}
main > div > div.bg-white {
background: rgba(255, 255, 255, 0.94);
backdrop-filter: blur(16px);
box-shadow: 0 24px 70px rgba(15, 23, 42, 0.08);
}
main section {
transition: transform 0.22s ease, box-shadow 0.22s ease, border-color 0.22s ease;
}
main section:hover {
transform: translateY(-2px);
box-shadow: 0 16px 35px rgba(15, 23, 42, 0.055);
border-color: rgba(16, 185, 129, 0.18);
}
input,
select {
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease, background-color 0.2s ease;
}
input:hover,
select:hover {
border-color: rgba(16, 185, 129, 0.45);
background-color: #ffffff;
}
input:focus,
select:focus {
transform: translateY(-1px);
}
#alert-box {
box-shadow: 0 14px 35px rgba(239, 68, 68, 0.08);
}
footer {
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
}
</style>
</head>
<body class="flex flex-col min-h-screen text-slate-800">
<!-- Navbar -->
<nav class="sticky top-0 z-50 bg-white/95 backdrop-blur-md border-b border-slate-100 px-6 py-4">
<div class="max-w-7xl mx-auto flex items-center justify-between gap-4">
<div class="flex items-center gap-4">
<div class="w-10 h-10 bg-emerald-100 rounded-xl flex items-center justify-center text-emerald-600 font-bold">
AS
</div>
<span class="font-black text-xl tracking-tighter text-emerald-950 uppercase italic">
ASGARD.
</span>
@ -112,30 +48,16 @@
Cek Rekomendasi
</a>
<!-- Menu untuk membuka hasil terakhir -->
<a href="#" onclick="goToLastResult(event)" class="hover:text-emerald-600 transition">
Hasil Rekomendasi
</a>
<!-- <a href="#" class="hover:text-emerald-600 transition">
Kontak BK
</a> -->
</div>
<div class="flex items-center gap-3">
<div class="hidden sm:flex items-center gap-3 bg-emerald-50 px-4 py-2 rounded-full border border-emerald-100">
<div class="w-2 h-2 bg-emerald-500 rounded-full animate-pulse"></div>
<span class="text-[10px] font-bold text-emerald-700 uppercase">
Siswa
</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>
<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>
</nav>
@ -153,7 +75,7 @@
</h1>
<p class="text-slate-500 mt-3 text-sm md:text-base leading-relaxed">
Isi data berikut jika ingin membuat rekomendasi baru. Jika ingin melihat hasil sebelumnya, klik menu <b>Hasil Rekomendasi</b> pada navbar.
Isi data berikut untuk mendapatkan rekomendasi jurusan.
</p>
</div>
@ -161,7 +83,6 @@
<div class="space-y-6">
<!-- Kondisi ekonomi -->
<section class="bg-slate-50 border border-slate-100 rounded-3xl p-5 md:p-6">
<h2 class="font-extrabold text-slate-800 mb-1">
💰 Kondisi Ekonomi
@ -176,15 +97,9 @@
id="gaji_ortu"
min="0"
placeholder="Contoh: 3000000"
class="w-full rounded-2xl border border-slate-200 bg-white px-4 py-4 text-base font-semibold text-slate-700 outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100"
>
<p class="text-xs text-slate-400 mt-2">
Isi dengan angka saja tanpa titik atau koma.
</p>
class="w-full rounded-2xl border border-slate-200 bg-white px-4 py-4 text-base font-semibold text-slate-700 outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
</section>
<!-- Minat dan bakat -->
<section class="bg-emerald-50/40 border border-emerald-100 rounded-3xl p-5 md:p-6">
<h2 class="font-extrabold text-emerald-800 mb-4">
🎯 Kuesioner Minat dan Bakat
@ -273,7 +188,6 @@ class="w-full rounded-2xl border border-slate-200 bg-white px-4 py-4 text-base f
</div>
</section>
<!-- Nilai rapor -->
<section class="bg-slate-50 border border-slate-100 rounded-3xl p-5 md:p-6">
<h2 class="font-extrabold text-slate-700 mb-2">
📚 Nilai Rapor
@ -284,146 +198,57 @@ class="w-full rounded-2xl border border-slate-200 bg-white px-4 py-4 text-base f
</p>
<div class="grid grid-cols-2 md:grid-cols-5 gap-4">
<div>
<label class="block text-xs font-bold text-slate-500 mb-2">Agama</label>
<input type="number" id="agama" min="0" max="100" placeholder="0 - 100" class="w-full rounded-xl border border-slate-200 bg-white px-3 py-3 font-semibold outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
</div>
@php
$mapel = [
'agama' => 'Agama',
'pkn' => 'PKN',
'bahasa_indonesia' => 'B. Indonesia',
'bahasa_inggris' => 'B. Inggris',
'matematika' => 'Matematika',
'fisika' => 'Fisika',
'kimia' => 'Kimia',
'biologi' => 'Biologi',
'sejarah' => 'Sejarah',
'geografi' => 'Geografi',
'ekonomi_mapel' => 'Ekonomi',
'sosiologi' => 'Sosiologi',
'seni_budaya' => 'Seni Budaya',
'pjok' => 'PJOK',
'informatika' => 'Informatika',
];
@endphp
<div>
<label class="block text-xs font-bold text-slate-500 mb-2">PKN</label>
<input type="number" id="pkn" min="0" max="100" placeholder="0 - 100" class="w-full rounded-xl border border-slate-200 bg-white px-3 py-3 font-semibold outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
</div>
@foreach ($mapel as $id => $label)
<div>
<label class="block text-xs font-bold text-slate-500 mb-2">
{{ $label }}
</label>
<div>
<label class="block text-xs font-bold text-slate-500 mb-2">B. Indonesia</label>
<input type="number" id="bahasa_indonesia" min="0" max="100" placeholder="0 - 100" class="w-full rounded-xl border border-slate-200 bg-white px-3 py-3 font-semibold outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
</div>
<div>
<label class="block text-xs font-bold text-slate-500 mb-2">B. Inggris</label>
<input type="number" id="bahasa_inggris" min="0" max="100" placeholder="0 - 100" class="w-full rounded-xl border border-slate-200 bg-white px-3 py-3 font-semibold outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
</div>
<div>
<label class="block text-xs font-bold text-slate-500 mb-2">Matematika</label>
<input type="number" id="matematika" min="0" max="100" placeholder="0 - 100" class="w-full rounded-xl border border-slate-200 bg-white px-3 py-3 font-semibold outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
</div>
<div>
<label class="block text-xs font-bold text-slate-500 mb-2">Fisika</label>
<input type="number" id="fisika" min="0" max="100" placeholder="0 - 100" class="w-full rounded-xl border border-slate-200 bg-white px-3 py-3 font-semibold outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
</div>
<div>
<label class="block text-xs font-bold text-slate-500 mb-2">Kimia</label>
<input type="number" id="kimia" min="0" max="100" placeholder="0 - 100" class="w-full rounded-xl border border-slate-200 bg-white px-3 py-3 font-semibold outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
</div>
<div>
<label class="block text-xs font-bold text-slate-500 mb-2">Biologi</label>
<input type="number" id="biologi" min="0" max="100" placeholder="0 - 100" class="w-full rounded-xl border border-slate-200 bg-white px-3 py-3 font-semibold outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
</div>
<div>
<label class="block text-xs font-bold text-slate-500 mb-2">Sejarah</label>
<input type="number" id="sejarah" min="0" max="100" placeholder="0 - 100" class="w-full rounded-xl border border-slate-200 bg-white px-3 py-3 font-semibold outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
</div>
<div>
<label class="block text-xs font-bold text-slate-500 mb-2">Geografi</label>
<input type="number" id="geografi" min="0" max="100" placeholder="0 - 100" class="w-full rounded-xl border border-slate-200 bg-white px-3 py-3 font-semibold outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
</div>
<div>
<label class="block text-xs font-bold text-slate-500 mb-2">Ekonomi</label>
<input type="number" id="ekonomi_mapel" min="0" max="100" placeholder="0 - 100" class="w-full rounded-xl border border-slate-200 bg-white px-3 py-3 font-semibold outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
</div>
<div>
<label class="block text-xs font-bold text-slate-500 mb-2">Sosiologi</label>
<input type="number" id="sosiologi" min="0" max="100" placeholder="0 - 100" class="w-full rounded-xl border border-slate-200 bg-white px-3 py-3 font-semibold outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
</div>
<div>
<label class="block text-xs font-bold text-slate-500 mb-2">Seni Budaya</label>
<input type="number" id="seni_budaya" min="0" max="100" placeholder="0 - 100" class="w-full rounded-xl border border-slate-200 bg-white px-3 py-3 font-semibold outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
</div>
<div>
<label class="block text-xs font-bold text-slate-500 mb-2">PJOK</label>
<input type="number" id="pjok" min="0" max="100" placeholder="0 - 100" class="w-full rounded-xl border border-slate-200 bg-white px-3 py-3 font-semibold outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
</div>
<div>
<label class="block text-xs font-bold text-slate-500 mb-2">Informatika</label>
<input type="number" id="informatika" min="0" max="100" placeholder="0 - 100" class="w-full rounded-xl border border-slate-200 bg-white px-3 py-3 font-semibold outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
</div>
<input
type="number"
id="{{ $id }}"
min="0"
max="100"
placeholder="0 - 100"
class="w-full rounded-xl border border-slate-200 bg-white px-3 py-3 font-semibold outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
</div>
@endforeach
</div>
</section>
<!-- Tombol -->
<div class="flex flex-col sm:flex-row gap-3 pt-2">
<a href="{{ route('student.profile') }}"
class="w-full sm:w-1/3 text-center bg-slate-100 hover:bg-slate-200 text-slate-600 font-bold py-4 rounded-2xl transition">
Kembali
</a>
<button id="btn-submit"
class="w-full sm:w-2/3 bg-emerald-600 hover:bg-emerald-700 text-white font-bold py-4 rounded-2xl transition shadow-lg shadow-emerald-200">
Cek Rekomendasi
</button>
</div>
<button
type="button"
id="btnSubmit"
class="w-full bg-emerald-600 hover:bg-emerald-700 text-white font-extrabold py-4 px-6 rounded-2xl transition">
Proses Rekomendasi
</button>
</div>
</div>
</div>
</main>
<!-- Footer -->
<footer class="bg-[#062C22] text-white py-16 px-6 mt-16">
<div class="max-w-7xl mx-auto grid grid-cols-1 md:grid-cols-3 gap-12">
<div class="space-y-6">
<span class="font-black text-2xl tracking-tighter uppercase italic">
ASGARD.
</span>
<p class="text-slate-300 text-sm leading-relaxed max-w-xs">
Sistem rekomendasi jurusan kuliah SMAN 4 Jember berbasis profil siswa dan data akademik.
</p>
</div>
<div>
<h5 class="font-bold text-xs uppercase tracking-widest text-emerald-400 mb-6">
Informasi
</h5>
<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">
<footer class="bg-[#062C22] text-white py-10 px-6 mt-10">
<div class="max-w-7xl mx-auto text-center">
<p class="text-[10px] font-medium uppercase tracking-[0.35em] text-slate-400">
© 2026 GALANG SEFIAN ADJI - POLITEKNIK NEGERI JEMBER
</p>
@ -431,39 +256,60 @@ class="w-full sm:w-2/3 bg-emerald-600 hover:bg-emerald-700 text-white font-bold
</footer>
<script>
function getAppBaseUrl() {
const origin = window.location.origin;
const segments = window.location.pathname.split('/').filter(Boolean);
const studentIndex = segments.indexOf('student');
if (studentIndex > 0) {
return origin + '/' + segments.slice(0, studentIndex).join('/');
}
return origin;
}
const APP_BASE_URL = getAppBaseUrl();
const routeLogin = `${APP_BASE_URL}/login`;
const routeDashboard = `${APP_BASE_URL}/student/profile`;
const routePrediction = `${APP_BASE_URL}/student/prediction`;
const routeAdminDashboard = `${APP_BASE_URL}/admin/dashboard`;
const apiLogoutUrl = `${APP_BASE_URL}/api/logout`;
const apiProsesRekomendasiUrl = `${APP_BASE_URL}/api/student/proses-rekomendasi`;
const studentResultUrlTemplate = `${APP_BASE_URL}/student/result/__ID__`;
const btnSubmit = document.getElementById('btnSubmit');
const alertBox = document.getElementById('alert-box');
const btnSubmit = document.getElementById('btn-submit');
const routeLogin = @json(route('login'));
const routeDashboard = @json(route('student.profile'));
const routePrediction = @json(route('student.prediction'));
const routeAdminDashboard = @json(route('admin.dashboard'));
function showError(message) {
alertBox.classList.remove('hidden');
alertBox.textContent = message;
window.scrollTo({
top: 0,
behavior: 'smooth'
});
}
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');
return localStorage.getItem('access_token') ||
localStorage.getItem('token') ||
sessionStorage.getItem('access_token') ||
sessionStorage.getItem('token') ||
'';
}
function getUserRole() {
return localStorage.getItem('user_role');
return localStorage.getItem('user_role') ||
localStorage.getItem('role') ||
sessionStorage.getItem('user_role') ||
sessionStorage.getItem('role') ||
'';
}
function clearLoginStorage() {
localStorage.removeItem('access_token');
localStorage.removeItem('token');
localStorage.removeItem('user_role');
localStorage.removeItem('role');
localStorage.removeItem('user_name');
localStorage.removeItem('name');
sessionStorage.removeItem('access_token');
sessionStorage.removeItem('token');
sessionStorage.removeItem('user_role');
sessionStorage.removeItem('role');
}
function redirectByRole(role) {
@ -472,7 +318,7 @@ function redirectByRole(role) {
return;
}
if (role === 'siswa') {
if (role === 'siswa' || role === 'student') {
window.location.href = routeDashboard;
return;
}
@ -480,6 +326,41 @@ function redirectByRole(role) {
window.location.href = routeLogin;
}
function makeResultUrl(id) {
return id ? studentResultUrlTemplate.replace('__ID__', encodeURIComponent(id)) : '';
}
function normalizeAppUrl(url) {
if (!url) return '';
const text = String(url);
if (text.startsWith('http://') || text.startsWith('https://')) {
return text;
}
if (text.startsWith('/')) {
return APP_BASE_URL + text;
}
return `${APP_BASE_URL}/${text}`;
}
async function parseJsonResponse(response) {
const text = await response.text();
try {
return text ? JSON.parse(text) : {};
} catch (error) {
return {
success: false,
message: text && text.trim().startsWith('<')
? 'Server mengembalikan halaman HTML, bukan JSON. Biasanya URL API salah, route API belum terdaftar, atau Flask API belum aktif.'
: (text || response.statusText || 'Respon server tidak valid.')
};
}
}
function getStoredLastResult() {
try {
const saved = localStorage.getItem('asgard_last_result');
@ -495,12 +376,12 @@ function goToLastResult(event) {
const saved = getStoredLastResult();
if (saved && saved.url) {
window.location.href = saved.url;
window.location.href = normalizeAppUrl(saved.url);
return;
}
if (saved && saved.id) {
window.location.href = `/student/result/${saved.id}`;
window.location.href = makeResultUrl(saved.id);
return;
}
@ -508,44 +389,41 @@ function goToLastResult(event) {
window.location.href = routePrediction;
}
function validateForm(payload) {
const scoreFields = [
'agama', 'pkn', 'bahasa_indonesia', 'bahasa_inggris', 'matematika',
'fisika', 'kimia', 'biologi', 'sejarah', 'geografi',
'ekonomi_mapel', 'sosiologi', 'seni_budaya', 'pjok', 'informatika'
];
async function handleLogout() {
const token = getAccessToken();
for (const field of scoreFields) {
const rawValue = document.getElementById(field).value;
if (rawValue === '') {
return `Nilai ${field.replaceAll('_', ' ')} wajib diisi.`;
}
const value = Number(payload[field]);
if (isNaN(value) || value < 0 || value > 100) {
return `Nilai ${field.replaceAll('_', ' ')} harus berada pada rentang 0 sampai 100.`;
if (token) {
try {
await fetch(apiLogoutUrl, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/json'
}
});
} catch (error) {
console.error('Logout error:', error);
}
}
if (document.getElementById('gaji_ortu').value === '') {
return 'Pendapatan gabungan orang tua wajib diisi.';
}
clearLoginStorage();
localStorage.removeItem('asgard_last_result');
window.location.href = routeLogin;
}
if (Number(payload.gaji_ortu) < 0) {
return 'Pendapatan gabungan orang tua tidak boleh bernilai negatif.';
}
function showError(message) {
alertBox.innerText = message;
alertBox.classList.remove('hidden');
}
const questionIds = ['q1', 'q2', 'q3', 'q4', 'q5'];
function hideError() {
alertBox.innerText = '';
alertBox.classList.add('hidden');
}
for (const id of questionIds) {
if (!document.getElementById(id).value) {
return 'Semua pertanyaan minat dan bakat wajib diisi.';
}
}
return null;
function getNumberValue(id) {
const value = document.getElementById(id).value;
return value === '' ? null : Number(value);
}
function calculateRiasecScores() {
@ -558,12 +436,10 @@ function calculateRiasecScores() {
c: 0
};
const questionIds = ['q1', 'q2', 'q3', 'q4', 'q5'];
questionIds.forEach(id => {
['q1', 'q2', 'q3', 'q4', 'q5'].forEach(id => {
const value = document.getElementById(id).value;
if (value && scores.hasOwnProperty(value)) {
if (value && Object.prototype.hasOwnProperty.call(scores, value)) {
scores[value] += 20;
}
});
@ -571,27 +447,54 @@ function calculateRiasecScores() {
return scores;
}
async function handleLogout() {
const token = getAccessToken();
function validateForm(payload) {
const scoreFields = [
'agama',
'pkn',
'bahasa_indonesia',
'bahasa_inggris',
'matematika',
'fisika',
'kimia',
'biologi',
'sejarah',
'geografi',
'ekonomi_mapel',
'sosiologi',
'seni_budaya',
'pjok',
'informatika'
];
if (token) {
try {
await fetch('/api/logout', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/json'
}
});
} catch (error) {
console.error('Logout error:', error);
for (const field of scoreFields) {
const rawValue = document.getElementById(field).value;
if (rawValue === '') {
return `Nilai ${field.replaceAll('_', ' ')} wajib diisi.`;
}
const value = Number(payload[field]);
if (Number.isNaN(value) || value < 0 || value > 100) {
return `Nilai ${field.replaceAll('_', ' ')} harus berada pada rentang 0 sampai 100.`;
}
}
localStorage.removeItem('access_token');
localStorage.removeItem('user_role');
localStorage.removeItem('asgard_last_result');
window.location.href = routeLogin;
if (document.getElementById('gaji_ortu').value === '') {
return 'Pendapatan gabungan orang tua wajib diisi.';
}
if (Number(payload.gaji_ortu) < 0) {
return 'Pendapatan gabungan orang tua tidak boleh bernilai negatif.';
}
for (const id of ['q1', 'q2', 'q3', 'q4', 'q5']) {
if (!document.getElementById(id).value) {
return 'Semua pertanyaan minat dan bakat wajib diisi.';
}
}
return null;
}
document.addEventListener('DOMContentLoaded', function () {
@ -603,9 +506,8 @@ function calculateRiasecScores() {
return;
}
if (role !== 'siswa') {
if (role !== 'siswa' && role !== 'student') {
redirectByRole(role);
return;
}
});
@ -625,12 +527,12 @@ function calculateRiasecScores() {
return;
}
if (role !== 'siswa') {
if (role !== 'siswa' && role !== 'student') {
redirectByRole(role);
return;
}
btnSubmit.innerHTML = 'Memproses Rekomendasi...';
btnSubmit.innerText = 'Memproses Rekomendasi...';
btnSubmit.disabled = true;
const scores = calculateRiasecScores();
@ -651,12 +553,14 @@ function calculateRiasecScores() {
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')
};
@ -664,34 +568,27 @@ function calculateRiasecScores() {
if (validationError) {
showError(validationError);
btnSubmit.innerHTML = 'Proses Rekomendasi';
btnSubmit.innerText = 'Proses Rekomendasi';
btnSubmit.disabled = false;
return;
}
try {
const response = await fetch('/api/student/proses-rekomendasi', {
const response = await fetch(apiProsesRekomendasiUrl, {
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();
const result = await parseJsonResponse(response);
if (response.ok && result.success) {
window.location.href = result.redirect_url;
return;
}
if (response.status === 401) {
if (response.status === 401 || response.status === 403) {
clearLoginStorage();
showError('Sesi login berakhir. Silakan login kembali.');
localStorage.removeItem('access_token');
localStorage.removeItem('user_role');
setTimeout(() => {
window.location.href = routeLogin;
@ -700,17 +597,31 @@ function calculateRiasecScores() {
return;
}
if (response.status === 403) {
showError(result.message || 'Anda tidak memiliki akses ke halaman ini.');
if (response.ok && result.success) {
const riwayatId = result.riwayat_id || result.id || result.data?.id || '';
const prediksiJurusan = result.prediksi_jurusan || result.hasil_rekomendasi_jurusan || result.data?.hasil_rekomendasi_jurusan || '';
const redirectUrl = normalizeAppUrl(result.redirect_url || makeResultUrl(riwayatId));
if (riwayatId && prediksiJurusan) {
localStorage.setItem('asgard_last_result', JSON.stringify({
id: riwayatId,
jurusan: prediksiJurusan,
url: redirectUrl,
tanggal: new Date().toLocaleString('id-ID')
}));
}
window.location.href = redirectUrl || routeDashboard;
return;
}
showError(result.message || 'Terjadi kesalahan saat memproses rekomendasi.');
} catch (error) {
showError('Kesalahan jaringan. Pastikan Laravel dan API Flask sedang berjalan.');
console.error(error);
showError(error.message || 'Kesalahan jaringan. Pastikan koneksi server dan Flask API PythonAnywhere aktif.');
} finally {
btnSubmit.innerHTML = 'Proses Rekomendasi';
btnSubmit.innerText = 'Proses Rekomendasi';
btnSubmit.disabled = false;
}
});

View File

@ -12,25 +12,75 @@
use App\Http\Controllers\Api\Student\DashboardStudentController;
use Illuminate\Support\Facades\Route;
Route::post('/login', [AuthController::class, 'login']);
/*
|--------------------------------------------------------------------------
| API Authentication & Public Routes
|--------------------------------------------------------------------------
*/
Route::post('/login', [AuthController::class, 'login'])->name('api.login');
/*
|--------------------------------------------------------------------------
| Test Koneksi Laravel ke Flask PythonAnywhere
|--------------------------------------------------------------------------
*/
Route::get('/python/health', [RecommendationApiController::class, 'health'])
->name('api.python.health');
/*
|--------------------------------------------------------------------------
| Protected API Routes (Membutuhkan Login / Token Sanctum)
|--------------------------------------------------------------------------
*/
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']);
/*
|--------------------------------------------------------------------------
| Admin Routes Group
|--------------------------------------------------------------------------
*/
Route::middleware('role:admin')->prefix('admin')->name('api.admin.')->group(function () {
// Dashboard Stats
Route::get('/dashboard-stats', [DashboardAdminController::class, 'index'])->name('dashboard-stats');
// CRUD Master Data Siswa
Route::apiResource('/siswa', SiswaController::class);
// CRUD Master Data Lainnya
Route::apiResource('/jurusan', JurusanController::class);
Route::apiResource('/guru', GuruController::class);
Route::apiResource('/alumni', AlumniController::class);
Route::apiResource('/riwayat', RiwayatController::class);
Route::apiResource('/prospek-kerja', ProspekKerjaController::class);
});
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']);
/*
|--------------------------------------------------------------------------
| Student (Siswa) Routes Group
|--------------------------------------------------------------------------
*/
Route::middleware('role:siswa')->prefix('student')->name('api.student.')->group(function () {
Route::get('/dashboard-stats', [DashboardStudentController::class, 'index'])
->name('dashboard-stats');
Route::post('/proses-rekomendasi', [RecommendationApiController::class, 'hitungPrediksi'])
->name('proses-rekomendasi');
Route::get('/hasil-rekomendasi/{id}', [RecommendationApiController::class, 'getHasilRekomendasi'])
->name('hasil-rekomendasi');
Route::post('/recommendation/predict', [RecommendationApiController::class, 'hitungPrediksi'])
->name('recommendation.predict');
Route::get('/recommendation/result/{id}', [RecommendationApiController::class, 'getHasilRekomendasi'])
->name('recommendation.result');
});
Route::post('/logout', [AuthController::class, 'logout']);
/*
|--------------------------------------------------------------------------
| Logout
|--------------------------------------------------------------------------
*/
Route::post('/logout', [AuthController::class, 'logout'])->name('api.logout');
});

View File

@ -29,14 +29,16 @@
Route::prefix('admin')->name('admin.')->group(function () {
Route::get('/dashboard', [AdminDashboardController::class, 'index'])->name('dashboard');
// PERBAIKAN: Format names menggunakan string tunggal 'siswa' seperti milik prospek-kerja
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');
Route::get('/riwayat', [RiwayatController::class, 'index'])->name('riwayat');
Route::get('/latih-model', function() {
// Logika latih model Anda
})->name('latih-model');
});