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'); return view('admin.siswa.siswacreate');
} }
public function edit($id) public function edit($id)
{ {
return view('admin.siswa.siswaedit', compact('id')); return view('admin.siswa.siswaedit', compact('id'));
} }

View File

@ -6,102 +6,195 @@
use App\Models\Alumni; use App\Models\Alumni;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
use Throwable;
class AlumniController extends Controller class AlumniController extends Controller
{ {
public function index(Request $request) public function index(Request $request)
{ {
$perPage = (int) $request->input('per_page', 7); try {
$perPage = in_array($perPage, [5, 7]) ? $perPage : 7; $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 !== '') { if ($search !== '') {
$query->where(function ($q) use ($search) { $query->where(function ($q) use ($search) {
$q->where('id_alumni', 'like', "%{$search}%") $q->where('id_alumni', 'like', "%{$search}%")
->orWhere('nama_alumni', 'like', "%{$search}%") ->orWhere('nama_alumni', 'like', "%{$search}%")
->orWhere('perguruan_tinggi', 'like', "%{$search}%") ->orWhere('perguruan_tinggi', 'like', "%{$search}%")
->orWhere('jurusan_diambil', '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) public function store(Request $request)
{ {
$validator = Validator::make($request->all(), [ try {
'nama_alumni' => 'required|string|max:255', $validator = Validator::make($request->all(), [
'perguruan_tinggi' => 'required|string', 'nama_alumni' => 'required|string|max:255',
'jurusan_diambil' => 'required|string', '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 harus diisi.', 'nama_alumni.required' => 'Nama alumni wajib diisi.',
'jurusan_diambil.required' => 'Jurusan yang diambil tidak boleh kosong.', 'perguruan_tinggi.required' => 'Nama perguruan tinggi wajib diisi.',
]); 'jurusan_diambil.required' => 'Jurusan yang diambil wajib diisi.',
]);
if ($validator->fails()) { if ($validator->fails()) {
return response()->json(['success' => false, 'errors' => $validator->errors()], 422); 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([ if (!$alumni) {
'success' => true, return response()->json([
'message' => 'Alumni ' . $alumni->id_alumni . ' berhasil ditambahkan.', 'success' => false,
'data' => $alumni 'message' => 'Data alumni tidak ditemukan.',
], 201); ], 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) public function update(Request $request, $id)
{ {
$alumni = Alumni::find($id); try {
$alumni = Alumni::find($id);
if (!$alumni) { if (!$alumni) {
return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404); 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) public function destroy($id)
{ {
$alumni = Alumni::find($id); try {
$alumni = Alumni::find($id);
if (!$alumni) { if (!$alumni) {
return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404); 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; namespace App\Http\Controllers\Api\Admin;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\User;
use App\Models\RiwayatRekomendasi; use App\Models\RiwayatRekomendasi;
use App\Models\Siswa;
use App\Models\User;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
use Exception; use Throwable;
class DashboardAdminController extends Controller class DashboardAdminController extends Controller
{ {
public function index(Request $request) public function index(Request $request)
{ {
try { 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) { if ($totalSiswa === 0) {
$totalSiswa = RiwayatRekomendasi::distinct('user_id')->count('user_id'); $totalSiswa = User::whereIn('role', ['siswa', 'student'])->count();
}
$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';
} }
$totalRekomendasi = Schema::hasTable($tableName) ? RiwayatRekomendasi::count() : 0;
$kolomAkurasi = $this->getKolomAkurasi($tableName);
$rataAkurasi = null; $rataAkurasi = null;
if ($kolomAkurasi) { if ($kolomAkurasi && Schema::hasTable($tableName)) {
$avg = RiwayatRekomendasi::whereNotNull($kolomAkurasi)->avg($kolomAkurasi); $avg = RiwayatRekomendasi::whereNotNull($kolomAkurasi)->avg($kolomAkurasi);
$rataAkurasi = $avg !== null ? round((float) $avg, 2) : null; $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.' ? 'Berdasarkan rata-rata tingkat keyakinan dari seluruh hasil rekomendasi.'
: 'Belum ada nilai keyakinan prediksi yang tersimpan. Lakukan prediksi baru terlebih dahulu.'; : '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) { if (Schema::hasTable($tableName) && Schema::hasColumn($tableName, 'hasil_rekomendasi_jurusan')) {
$selectDistribusi .= ', ROUND(AVG(' . $kolomAkurasi . '), 2) as rata_akurasi'; $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([ return response()->json([
'success' => true, 'success' => true,
'message' => 'Statistik dashboard berhasil dimuat.',
'data' => [ 'data' => [
'total_siswa' => $totalSiswa, 'total_siswa' => $totalSiswa,
'rekomendasi_diproses' => $totalRekomendasi, 'rekomendasi_diproses' => $totalRekomendasi,
'rata_rata_akurasi' => $rataAkurasi, 'rata_rata_akurasi' => $rataAkurasi,
'akurasi_status' => $akurasiStatus, 'akurasi_status' => $akurasiStatus,
'distribusi_jurusan' => $distribusiJurusan, 'distribusi_jurusan' => $distribusiJurusan,
] ],
], 200); ], 200);
} catch (Throwable $e) {
} catch (Exception $e) {
return response()->json([ return response()->json([
'success' => false, 'success' => false,
'message' => 'Error: ' . $e->getMessage() 'message' => 'Gagal memuat statistik dashboard.',
'error' => $e->getMessage(),
], 500); ], 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\Http\Controllers\Controller;
use App\Models\Guru; use App\Models\Guru;
use App\Models\User;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\DB;
use Throwable;
class GuruController extends Controller class GuruController extends Controller
{ {
/**
* Display a listing of the resource.
*/
public function index(Request $request) public function index(Request $request)
{ {
$perPage = (int) $request->input('per_page', 7); try {
$perPage = in_array($perPage, [5, 7]) ? $perPage : 7; $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 !== '') { if ($search !== '') {
$query->where(function ($q) use ($search) { $query->where(function ($q) use ($search) {
$q->where('id_guru', 'like', "%{$search}%") $q->where('id_guru', 'like', "%{$search}%")
->orWhere('nama_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) public function store(Request $request)
{ {
$validator = Validator::make($request->all(), [ $validator = Validator::make($request->all(), [
'nama_guru' => 'required|string|max:255', 'nama_guru' => 'required|string|max:255',
], [ 'username' => 'required|string|max:255|unique:users,username',
'nama_guru.required' => 'Nama guru tidak boleh kosong.', 'email' => 'required|string|email|max:255|unique:users,email',
'password' => 'required|string|min:8',
]); ]);
if ($validator->fails()) { 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([ try {
'success' => true, // Create User Akun
'message' => 'Guru ' . $guru->id_guru . ' berhasil ditambahkan.', $user = User::create([
'data' => $guru 'name' => $request->nama_guru,
], 201); '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) public function update(Request $request, $id)
{ {
$guru = Guru::find($id); try {
$guru = Guru::find($id);
if (!$guru) { if (!$guru) {
return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404); 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) public function destroy($id)
{ {
$guru = Guru::find($id); try {
$guru = Guru::find($id);
if (!$guru) { if (!$guru) {
return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404); 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 App\Models\Jurusan;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Throwable;
class JurusanController extends Controller class JurusanController extends Controller
{ {
public function index(Request $request) public function index(Request $request)
{ {
$perPage = (int) $request->input('per_page', 7); try {
$perPage = in_array($perPage, [5, 7]) ? $perPage : 7; $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 !== '') { if ($search !== '') {
$query->where(function ($q) use ($search) { $query->where(function ($q) use ($search) {
$q->where('nama_jurusan', 'like', "%{$search}%") $q->where('nama_jurusan', 'like', "%{$search}%")
->orWhere('rumpun', 'like', "%{$search}%") ->orWhere('rumpun', 'like', "%{$search}%")
->orWhere('prospek_karir', '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) public function store(Request $request)
{ {
$validator = Validator::make($request->all(), [ try {
'nama_jurusan' => 'required|string|max:255', $validator = Validator::make($request->all(), [
'rumpun' => 'required|string', '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()) { if ($validator->fails()) {
return response()->json(['success' => false, 'errors' => $validator->errors()], 422); 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) public function show($id)
{ {
$jurusan = Jurusan::find($id); try {
$jurusan = Jurusan::find($id);
if (!$jurusan) { if (!$jurusan) {
return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404); 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) public function update(Request $request, $id)
{ {
$jurusan = Jurusan::find($id); try {
$jurusan = Jurusan::find($id);
if (!$jurusan) { if (!$jurusan) {
return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404); 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) public function destroy($id)
{ {
$jurusan = Jurusan::find($id); try {
$jurusan = Jurusan::find($id);
if (!$jurusan) { if (!$jurusan) {
return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404); 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 App\Models\ProspekKerja;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
use Throwable;
class ProspekKerjaController extends Controller class ProspekKerjaController extends Controller
{ {
public function index(Request $request) public function index(Request $request)
{ {
$perPage = (int) $request->input('per_page', 7); try {
$perPage = in_array($perPage, [5, 7]) ? $perPage : 7; $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 !== '') { if ($search !== '') {
$query->where(function ($q) use ($search) { $query->where(function ($q) use ($search) {
$q->where('nama_profesi', 'like', "%{$search}%") $q->where('nama_profesi', 'like', "%{$search}%")
->orWhere('range_gaji', 'like', "%{$search}%") ->orWhere('range_gaji', 'like', "%{$search}%")
->orWhere('deskripsi_pekerjaan', 'like', "%{$search}%") ->orWhere('deskripsi_pekerjaan', 'like', "%{$search}%")
->orWhereHas('jurusan', function ($jurusan) use ($search) { ->orWhereHas('jurusan', function ($jurusan) use ($search) {
$jurusan->where('nama_jurusan', 'like', "%{$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) public function store(Request $request)
{ {
$validator = Validator::make($request->all(), [ try {
'jurusan_id' => 'required|exists:jurusans,id', $validator = Validator::make($request->all(), [
'nama_profesi' => 'required|string|max:255', 'jurusan_id' => 'required|integer|exists:jurusans,id',
'range_gaji' => 'nullable|string|max:255', 'nama_profesi' => 'required|string|max:255',
'deskripsi_pekerjaan' => 'nullable|string', '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()) { if ($validator->fails()) {
return response()->json(['success' => false, 'errors' => $validator->errors()], 422); 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) public function show($id)
{ {
$prospek = ProspekKerja::find($id); try {
$prospek = ProspekKerja::with('jurusan')->find($id);
if (!$prospek) { if (!$prospek) {
return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404); 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) public function update(Request $request, $id)
{ {
$prospek = ProspekKerja::find($id); try {
$prospek = ProspekKerja::find($id);
if (!$prospek) { if (!$prospek) {
return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404); 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) public function destroy($id)
{ {
$prospek = ProspekKerja::find($id); try {
$prospek = ProspekKerja::find($id);
if (!$prospek) { if (!$prospek) {
return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404); 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 App\Models\RiwayatRekomendasi;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
use Exception; use Throwable;
class RiwayatController extends Controller class RiwayatController extends Controller
{ {
@ -14,7 +14,7 @@ public function index(Request $request)
{ {
try { try {
$perPage = (int) $request->input('per_page', 7); $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', '')); $search = trim((string) $request->input('search', ''));
@ -49,6 +49,7 @@ public function index(Request $request)
return response()->json([ return response()->json([
'success' => true, 'success' => true,
'message' => 'Data riwayat berhasil dimuat.',
'data' => $riwayat->items(), 'data' => $riwayat->items(),
'current_page' => $riwayat->currentPage(), 'current_page' => $riwayat->currentPage(),
'last_page' => $riwayat->lastPage(), 'last_page' => $riwayat->lastPage(),
@ -57,11 +58,11 @@ public function index(Request $request)
'from' => $riwayat->firstItem(), 'from' => $riwayat->firstItem(),
'to' => $riwayat->lastItem(), 'to' => $riwayat->lastItem(),
], 200); ], 200);
} catch (Throwable $e) {
} catch (Exception $e) {
return response()->json([ return response()->json([
'success' => false, 'success' => false,
'message' => 'Error: ' . $e->getMessage() 'message' => 'Gagal memuat data riwayat.',
'error' => $e->getMessage(),
], 500); ], 500);
} }
} }
@ -69,10 +70,18 @@ public function index(Request $request)
public function show($id) public function show($id)
{ {
try { 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([ return response()->json([
'success' => true, 'success' => true,
'message' => 'Detail riwayat berhasil ditemukan.',
'data' => [ 'data' => [
'id' => $detail->id, 'id' => $detail->id,
'tanggal' => $detail->created_at ? $detail->created_at->format('d M Y H:i:s') : '-', '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), 'kondisi_ekonomi' => $this->getLabelEkonomi($detail->gaji_ortu),
'gaji_ortu' => $detail->gaji_ortu, 'gaji_ortu' => $detail->gaji_ortu,
'nilai_ringkas' => [ 'nilai_ringkas' => [
'Matematika' => $detail->matematika, 'Agama' => $detail->agama,
'PKN' => $detail->pkn,
'B. Indonesia' => $detail->bahasa_indonesia, 'B. Indonesia' => $detail->bahasa_indonesia,
'B. Inggris' => $detail->bahasa_inggris, 'B. Inggris' => $detail->bahasa_inggris,
'Matematika' => $detail->matematika,
'Fisika' => $detail->fisika, 'Fisika' => $detail->fisika,
'Kimia' => $detail->kimia, 'Kimia' => $detail->kimia,
'Biologi' => $detail->biologi, 'Biologi' => $detail->biologi,
'Sejarah' => $detail->sejarah,
'Geografi' => $detail->geografi,
'Ekonomi' => $detail->ekonomi_mapel, 'Ekonomi' => $detail->ekonomi_mapel,
'Sosiologi' => $detail->sosiologi,
'Seni Budaya' => $detail->seni_budaya,
'PJOK' => $detail->pjok,
'Informatika' => $detail->informatika, 'Informatika' => $detail->informatika,
], ],
'riasec' => [ 'riasec' => [
@ -102,14 +118,41 @@ public function show($id)
'Enterprising' => $detail->riasec_e, 'Enterprising' => $detail->riasec_e,
'Conventional' => $detail->riasec_c, 'Conventional' => $detail->riasec_c,
], ],
] ],
], 200); ], 200);
} catch (Throwable $e) {
} catch (Exception $e) {
return response()->json([ return response()->json([
'success' => false, 'success' => false,
'message' => 'Data detail tidak ditemukan: ' . $e->getMessage() 'message' => 'Terjadi kesalahan saat mengambil detail riwayat.',
], 404); '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(); $tableName = (new RiwayatRekomendasi())->getTable();
if (!Schema::hasColumn($tableName, 'akurasi_prediksi')) { foreach (['akurasi_prediksi', 'probabilitas', 'akurasi'] as $column) {
return null; if (Schema::hasColumn($tableName, $column) && $riwayat->{$column} !== null) {
return round((float) $riwayat->{$column}, 2);
}
} }
if ($riwayat->akurasi_prediksi === null) { return null;
return null;
}
return round((float) $riwayat->akurasi_prediksi, 2);
} }
private function getRataNilai($riwayat): float private function getRataNilai($riwayat): float
@ -148,7 +189,7 @@ private function getRataNilai($riwayat): float
$riwayat->informatika, $riwayat->informatika,
]; ];
$nilai = array_filter($nilai, fn ($item) => $item !== null); $nilai = array_filter($nilai, fn ($item) => $item !== null && $item !== '');
if (count($nilai) === 0) { if (count($nilai) === 0) {
return 0; return 0;
@ -175,6 +216,10 @@ private function getMinatUtama($riwayat): string
private function getLabelEkonomi($gaji): string private function getLabelEkonomi($gaji): string
{ {
if ($gaji === null || $gaji === '') {
return '-';
}
$gaji = (float) $gaji; $gaji = (float) $gaji;
if ($gaji < 2000000) { if ($gaji < 2000000) {

View File

@ -4,94 +4,99 @@
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Siswa; use App\Models\Siswa;
use App\Models\User;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator; 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 class SiswaController extends Controller
{ {
public function index(Request $request) public function index(Request $request)
{ {
$perPage = (int) $request->input('per_page', 7); try {
$perPage = in_array($perPage, [5, 7]) ? $perPage : 7; $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 !== '') { $siswa = $query->paginate($perPage);
$query->where(function ($q) use ($search) {
$q->where('id_siswa', 'like', "%{$search}%") return response()->json(['success' => true, 'data' => $siswa], 200);
->orWhere('nama_lengkap', 'like', "%{$search}%") } catch (Throwable $e) {
->orWhere('nisn', 'like', "%{$search}%") return response()->json(['success' => false, 'message' => $e->getMessage()], 500);
->orWhere('kelas', 'like', "%{$search}%")
->orWhere('peminatan', 'like', "%{$search}%")
->orWhereHas('user', function ($user) use ($search) {
$user->where('name', 'like', "%{$search}%")
->orWhere('email', 'like', "%{$search}%");
})
->orWhereHas('jurusan', function ($jurusan) use ($search) {
$jurusan->where('nama_jurusan', 'like', "%{$search}%");
});
});
} }
$siswa = $query->paginate($perPage);
return response()->json([
'success' => true,
'data' => $siswa->items(),
'current_page' => $siswa->currentPage(),
'last_page' => $siswa->lastPage(),
'per_page' => $siswa->perPage(),
'total' => $siswa->total(),
'from' => $siswa->firstItem(),
'to' => $siswa->lastItem(),
], 200);
} }
public function store(Request $request) public function store(Request $request)
{ {
$validator = Validator::make($request->all(), [ $validator = Validator::make($request->all(), [
'user_id' => 'required|exists:users,id|unique:siswas,user_id', 'username' => ['required', Rule::unique(User::class, 'username')],
'jurusan_id' => 'required|exists:jurusans,id', 'password' => 'required|min:6',
'nama_lengkap' => 'required|string|max:255', 'nama_lengkap' => 'required',
'nisn' => 'required|string|unique:siswas,nisn', 'nisn' => ['required', Rule::unique(Siswa::class, 'nisn')],
'kelas' => 'required|string', 'kelas' => 'required',
'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.',
]); ]);
if ($validator->fails()) { if ($validator->fails()) {
return response()->json([ return response()->json(['success' => false, 'message' => 'Validasi gagal', 'errors' => $validator->errors()], 422);
'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([ $user = User::create([
'success' => true, 'name' => $request->nama_lengkap,
'message' => 'Siswa ' . $siswa->id_siswa . ' (' . $siswa->nama_lengkap . ') berhasil ditambahkan.', 'username' => $request->username,
'data' => $siswa 'email' => $otomatisEmail,
], 201); '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) public function show($id)
{ {
$siswa = Siswa::find($id); try {
$siswa = Siswa::with('user')->find($id);
if (!$siswa) { if (!$siswa) {
return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404); 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) public function update(Request $request, $id)
@ -99,39 +104,85 @@ public function update(Request $request, $id)
$siswa = Siswa::find($id); $siswa = Siswa::find($id);
if (!$siswa) { 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(), [ $validator = Validator::make($request->all(), [
'nama_lengkap' => 'required|string|max:255', 'username' => [
'nisn' => 'required|string|unique:siswas,nisn,' . $id, 'nullable',
'kelas' => 'required|string', Rule::unique(User::class, 'username')->ignore($siswa->user_id)
], [ ],
'nisn.unique' => 'Gagal Update! NISN ' . $request->nisn . ' sudah digunakan siswa lain.' 'nisn' => [
'required',
Rule::unique(Siswa::class, 'nisn')->ignore($siswa->id)
],
'nama_lengkap' => 'required',
'kelas' => 'required',
]); ]);
if ($validator->fails()) { 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([ if ($request->filled('username')) {
'success' => true, $user->username = $request->username;
'message' => 'Data ' . $siswa->id_siswa . ' berhasil diperbarui' $usernameClean = str_replace(' ', '', $request->username);
], 200); $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) public function destroy($id)
{ {
$siswa = Siswa::find($id); try {
$siswa = Siswa::find($id);
if (!$siswa) { if (!$siswa) {
return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404); 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 App\Models\User;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
class AuthController extends Controller class AuthController extends Controller
{ {
@ -26,10 +25,8 @@ public function login(Request $request)
], 401); ], 401);
} }
// Hapus token lama
$user->tokens()->delete(); $user->tokens()->delete();
// Buat token baru
$token = $user->createToken('auth_token')->plainTextToken; $token = $user->createToken('auth_token')->plainTextToken;
return response()->json([ return response()->json([
@ -37,8 +34,9 @@ public function login(Request $request)
'message' => 'Login berhasil', 'message' => 'Login berhasil',
'access_token' => $token, 'access_token' => $token,
'token_type' => 'Bearer', 'token_type' => 'Bearer',
'role' => $user->role, // Penting untuk redirect di frontend 'role' => $user->role,
'user' => [ 'user' => [
'id' => $user->id,
'name' => $user->name, 'name' => $user->name,
'username' => $user->username, 'username' => $user->username,
] ]
@ -47,7 +45,15 @@ public function login(Request $request)
public function logout(Request $request) public function logout(Request $request)
{ {
$request->user()->currentAccessToken()->delete(); $token = $request->user()?->currentAccessToken();
return response()->json(['message' => 'Berhasil logout']);
if ($token) {
$token->delete();
}
return response()->json([
'success' => true,
'message' => 'Berhasil logout'
]);
} }
} }

View File

@ -14,8 +14,11 @@ public function index()
try { try {
$user = Auth::user(); $user = Auth::user();
if (!$user) { if (! $user) {
return response()->json(['success' => false, 'message' => 'Unauthorized'], 401); return response()->json([
'success' => false,
'message' => 'Unauthorized'
], 401);
} }
$totalAktivitas = RiwayatRekomendasi::where('user_id', $user->id)->count(); $totalAktivitas = RiwayatRekomendasi::where('user_id', $user->id)->count();
@ -32,7 +35,7 @@ public function index()
return [ return [
'id' => $item->id, 'id' => $item->id,
'jurusan' => $item->hasil_rekomendasi_jurusan, '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() ->get()
->map(function ($item, $index) { ->map(function ($item, $index) {
$mapel = [ $mapel = [
'agama', 'pkn', 'bahasa_indonesia', 'bahasa_inggris', 'matematika', 'agama',
'fisika', 'kimia', 'biologi', 'sejarah', 'geografi', 'pkn',
'ekonomi_mapel', 'sosiologi', 'seni_budaya', 'pjok', 'informatika' 'bahasa_indonesia',
'bahasa_inggris',
'matematika',
'fisika',
'kimia',
'biologi',
'sejarah',
'geografi',
'ekonomi_mapel',
'sosiologi',
'seni_budaya',
'pjok',
'informatika'
]; ];
$totalNilai = 0; $totalNilai = 0;
foreach ($mapel as $m) { 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 [ return [
'label' => 'Tes ' . ($index + 1), 'label' => 'Tes ' . ($index + 1),
@ -63,7 +79,9 @@ public function index()
'success' => true, 'success' => true,
'data' => [ 'data' => [
'total_aktivitas' => $totalAktivitas, '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, 'aktivitas_terbaru' => $aktivitasTerbaru,
'tren_nilai' => $trenNilai 'tren_nilai' => $trenNilai
] ]

View File

@ -4,18 +4,26 @@
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\RiwayatRekomendasi; use App\Models\RiwayatRekomendasi;
use App\Models\Jurusan;
use App\Services\PythonRecommendationService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
class RecommendationApiController extends Controller 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(); $user = Auth::user();
if (!$user) { if (! $user) {
return response()->json([ return response()->json([
'success' => false, 'success' => false,
'message' => 'User belum login.' 'message' => 'User belum login.'
@ -49,49 +57,52 @@ public function hitungPrediksi(Request $request)
'gaji_ortu' => 'required|numeric|min:0', 'gaji_ortu' => 'required|numeric|min:0',
]); ]);
$payload = $validated; $payloadPython = $validated;
$payload['ekonomi_orang_tua'] = self::getLabelEkonomiSingkat($validated['gaji_ortu']);
$payloadPython['ekonomi_orang_tua'] = self::getLabelEkonomiSingkat($validated['gaji_ortu']);
unset($payloadPython['gaji_ortu']);
try { try {
$response = Http::timeout(15) $pythonResult = $pythonService->predict($payloadPython);
->acceptJson()
->post('http://127.0.0.1:5000/predict', $payload);
if (!$response->successful()) { if (! $pythonResult['success']) {
return response()->json([ return response()->json([
'success' => false, '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); ], 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([ return response()->json([
'success' => false, 'success' => false,
'message' => 'ML Engine Error: ' . ($responseData['message'] ?? 'Unknown Error') 'message' => 'ML Engine Error: ' . ($responseData['message'] ?? 'Unknown Error')
], 500); ], 500);
} }
$teksJurusan = $responseData['prediksi_jurusan'] ?? null; $teksJurusan = $responseData['prediksi_jurusan']
?? $responseData['rekomendasi']
?? $responseData['jurusan']
?? null;
if (!$teksJurusan) { if (! $teksJurusan) {
return response()->json([ return response()->json([
'success' => false, 'success' => false,
'message' => 'Prediksi jurusan tidak ditemukan pada respons Flask.' 'message' => 'Prediksi jurusan tidak ditemukan pada respons Flask.'
], 500); ], 500);
} }
/* $akurasiPrediksi = self::ambilAkurasiDariResponse($responseData, $teksJurusan);
* Ambil nilai probabilitas dari Flask.
* Contoh respons Flask yang disarankan:
* {
* "success": true,
* "prediksi_jurusan": "Teknik Informatika",
* "probabilitas": 86.45
* }
*/
$akurasiPrediksi = self::normalisasiAkurasi($responseData['probabilitas'] ?? null);
$dataRiwayat = [ $dataRiwayat = [
'user_id' => $user->id, 'user_id' => $user->id,
@ -123,16 +134,16 @@ public function hitungPrediksi(Request $request)
'hasil_rekomendasi_jurusan' => $teksJurusan, '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(); $tableName = (new RiwayatRekomendasi())->getTable();
if (Schema::hasColumn($tableName, 'akurasi_prediksi')) { if (Schema::hasColumn($tableName, 'akurasi_prediksi')) {
$dataRiwayat['akurasi_prediksi'] = $akurasiPrediksi; $dataRiwayat['akurasi_prediksi'] = $akurasiPrediksi;
} }
if (Schema::hasColumn($tableName, 'probabilitas')) {
$dataRiwayat['probabilitas'] = $akurasiPrediksi;
}
$riwayat = RiwayatRekomendasi::create($dataRiwayat); $riwayat = RiwayatRekomendasi::create($dataRiwayat);
return response()->json([ return response()->json([
@ -140,7 +151,9 @@ public function hitungPrediksi(Request $request)
'message' => 'Prediksi berhasil dibuat.', 'message' => 'Prediksi berhasil dibuat.',
'redirect_url' => route('student.result', ['id' => $riwayat->id]), 'redirect_url' => route('student.result', ['id' => $riwayat->id]),
'prediksi_jurusan' => $teksJurusan, 'prediksi_jurusan' => $teksJurusan,
'akurasi_prediksi' => $akurasiPrediksi 'akurasi_prediksi' => $akurasiPrediksi,
'top_predictions' => $responseData['top_predictions'] ?? null,
'riwayat_id' => $riwayat->id,
]); ]);
} catch (\Throwable $e) { } catch (\Throwable $e) {
@ -153,7 +166,16 @@ public function hitungPrediksi(Request $request)
public function getHasilRekomendasi($id) 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([ return response()->json([
'success' => true, 'success' => true,
@ -166,7 +188,7 @@ public static function formatHasilData(RiwayatRekomendasi $riwayat): array
return [ return [
'id' => $riwayat->id, 'id' => $riwayat->id,
'hasil_rekomendasi_jurusan' => $riwayat->hasil_rekomendasi_jurusan, '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), 'label_ekonomi' => self::getLabelEkonomiDetail($riwayat->gaji_ortu),
'minat_utama' => self::getMinatUtamaLabel($riwayat), 'minat_utama' => self::getMinatUtamaLabel($riwayat),
'prospek_kerja' => self::getProspekKerjaText($riwayat->hasil_rekomendasi_jurusan), '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 private static function normalisasiAkurasi($value): ?float
{ {
if ($value === null || $value === '') { if ($value === null || $value === '') {
@ -194,28 +244,16 @@ private static function normalisasiAkurasi($value): ?float
$value = trim($value); $value = trim($value);
} }
if (!is_numeric($value)) { if (! is_numeric($value)) {
return null; return null;
} }
$number = (float) $value; $number = (float) $value;
/* if ($number >= 0 && $number <= 1) {
* Jika Flask mengirim 0.86, ubah menjadi 86.
* Jika Flask mengirim 86.45, tetap 86.45.
*/
if ($number <= 1) {
$number = $number * 100; $number = $number * 100;
} }
if ($number < 0) {
$number = 0;
}
if ($number > 100) {
$number = 100;
}
return round($number, 2); return round($number, 2);
} }
@ -225,7 +263,9 @@ public static function getLabelEkonomiSingkat($gaji): string
if ($gaji < 2000000) { if ($gaji < 2000000) {
return 'Kurang Mampu'; return 'Kurang Mampu';
} elseif ($gaji <= 5000000) { }
if ($gaji <= 5000000) {
return 'Menengah'; return 'Menengah';
} }
@ -237,60 +277,65 @@ public static function getLabelEkonomiDetail($gaji): string
$gaji = (float) $gaji; $gaji = (float) $gaji;
if ($gaji < 2000000) { if ($gaji < 2000000) {
return 'Kurang Mampu (< Rp 2.000.000)'; return 'Ekonomi Kurang Mampu';
} elseif ($gaji <= 5000000) {
return 'Menengah (Rp 2.000.000 - Rp 5.000.000)';
} }
return 'Mampu (> Rp 5.000.000)'; if ($gaji <= 5000000) {
return 'Ekonomi Menengah';
}
return 'Ekonomi Mampu';
} }
public static function getMinatUtamaLabel(RiwayatRekomendasi $riwayat): string public static function getMinatUtamaLabel(RiwayatRekomendasi $riwayat): string
{ {
$riasecScores = [ $scores = [
'r' => (float) $riwayat->riasec_r, 'Realistic' => (float) $riwayat->riasec_r,
'i' => (float) $riwayat->riasec_i, 'Investigative' => (float) $riwayat->riasec_i,
'a' => (float) $riwayat->riasec_a, 'Artistic' => (float) $riwayat->riasec_a,
's' => (float) $riwayat->riasec_s, 'Social' => (float) $riwayat->riasec_s,
'e' => (float) $riwayat->riasec_e, 'Enterprising' => (float) $riwayat->riasec_e,
'c' => (float) $riwayat->riasec_c, 'Conventional' => (float) $riwayat->riasec_c,
]; ];
arsort($riasecScores); arsort($scores);
$topCode = array_key_first($riasecScores);
$labels = [ return array_key_first($scores) ?? 'Belum diketahui';
'r' => 'Teknis & Lapangan',
'i' => 'Sains, Riset & Analitis',
'a' => 'Kreatif & Desain',
's' => 'Sosial & Pelayanan',
'e' => 'Bisnis & Kepemimpinan',
'c' => 'Administratif & Terstruktur',
];
return $labels[$topCode] ?? 'Belum teridentifikasi';
} }
public static function getProspekKerjaText($jurusan): string
{
$jurusan = strtolower((string) $jurusan);
if (str_contains($jurusan, 'ekonomi') || str_contains($jurusan, 'manajemen') || str_contains($jurusan, 'bisnis')) { public static function getProspekKerjaText(?string $jurusan): string
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 (! $jurusan) {
return 'Prospek kerja belum tersedia.';
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.';
} }
// 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(); parent::boot();
// Otomatis membuat id_alumni yang unik dengan aman saat data dibuat
static::creating(function ($model) { static::creating(function ($model) {
$latest = static::orderBy('id', 'desc')->first(); if (empty($model->id_alumni)) {
if (!$latest) { $nextNumber = ((int) static::max('id')) + 1;
$model->id_alumni = 'AL01'; $candidate = 'ALM' . str_pad($nextNumber, 4, '0', STR_PAD_LEFT);
} else {
$number = intval(substr($latest->id_alumni, 2)) + 1; while (static::where('id_alumni', $candidate)->exists()) {
$model->id_alumni = 'AL' . str_pad($number, 2, '0', STR_PAD_LEFT); $nextNumber++;
$candidate = 'ALM' . str_pad($nextNumber, 4, '0', STR_PAD_LEFT);
}
$model->id_alumni = $candidate;
} }
}); });
} }

View File

@ -3,22 +3,37 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Guru extends Model 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() protected static function boot()
{ {
parent::boot(); parent::boot();
static::creating(function ($model) { static::creating(function ($model) {
$latest = static::orderBy('id', 'desc')->first(); if (empty($model->id_guru)) {
if (!$latest) { $nextNumber = ((int) static::max('id')) + 1;
$model->id_guru = 'GR01'; $candidate = 'GRU' . str_pad($nextNumber, 4, '0', STR_PAD_LEFT);
} else {
$number = intval(substr($latest->id_guru, 2)) + 1; while (static::where('id_guru', $candidate)->exists()) {
$model->id_guru = 'GR' . str_pad($number, 2, '0', STR_PAD_LEFT); $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 class Jurusan extends Model
{ {
protected $fillable = ['nama_jurusan', 'rumpun', 'deskripsi']; protected $fillable = ['nama_jurusan', 'rumpun',];
// Relasi: Satu Jurusan memiliki banyak Prospek Kerja // Relasi: Satu Jurusan memiliki banyak Prospek Kerja
public function prospeks() public function prospeks()

View File

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

View File

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

View File

@ -19,18 +19,26 @@
<label>Nama Lengkap Alumni</label> <label>Nama Lengkap Alumni</label>
<input type="text" id="nama_alumni" class="form-control input-custom" placeholder="Contoh: Ahmad Fauzi" required> <input type="text" id="nama_alumni" class="form-control input-custom" placeholder="Contoh: Ahmad Fauzi" required>
</div> </div>
<div class="col-md-12 form-group-custom"> <div class="col-md-12 form-group-custom">
<label>Perguruan Tinggi</label> <label>Perguruan Tinggi</label>
<input type="text" id="perguruan_tinggi" class="form-control input-custom" placeholder="Contoh: Universitas Indonesia" required> <input type="text" id="perguruan_tinggi" class="form-control input-custom" placeholder="Contoh: Universitas Indonesia" required>
</div> </div>
<div class="col-md-12 form-group-custom"> <div class="col-md-12 form-group-custom">
<label>Jurusan Kuliah yang Diambil</label> <label>Jurusan Kuliah yang Diambil</label>
<input type="text" id="jurusan_diambil" class="form-control input-custom" placeholder="Contoh: Teknik Informatika" required> <input type="text" id="jurusan_diambil" class="form-control input-custom" placeholder="Contoh: Teknik Informatika" required>
</div> </div>
</div> </div>
<div class="mt-5 d-flex gap-2 justify-content-end"> <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> <button type="submit" id="btnSave" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">
<a href="{{ route('admin.alumni') }}" class="btn btn-light px-4" style="border-radius: 10px;">Batal</a> Simpan Alumni
</button>
<a href="{{ route('admin.alumni') }}" class="btn btn-light px-4" style="border-radius: 10px;">
Batal
</a>
</div> </div>
</form> </form>
</div> </div>
@ -39,34 +47,120 @@
</div> </div>
<script> <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(); 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'); const btn = document.getElementById('btnSave');
btn.disabled = true; btn.disabled = true;
btn.innerText = 'Menyimpan...';
const payload = { const payload = {
nama_alumni: document.getElementById('nama_alumni').value, nama_alumni: getValue('nama_alumni'),
perguruan_tinggi: document.getElementById('perguruan_tinggi').value, perguruan_tinggi: getValue('perguruan_tinggi'),
jurusan_diambil: document.getElementById('jurusan_diambil').value jurusan_diambil: getValue('jurusan_diambil')
}; };
try { try {
const response = await fetch('/api/admin/alumni', { const response = await fetch(API_ALUMNI_URL, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('access_token'), 'Authorization': 'Bearer ' + token,
'Accept': 'application/json' 'Accept': 'application/json'
}, },
body: JSON.stringify(payload) body: JSON.stringify(payload)
}); });
if (response.ok) { const result = await parseJsonResponse(response);
Swal.fire({ icon: 'success', title: 'Berhasil!', confirmButtonColor: '#2D6A4F' })
.then(() => { window.location.href = "{{ route('admin.alumni') }}"; }); 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> </script>
@endsection @endsection

View File

@ -15,23 +15,32 @@
<div class="card-body p-5"> <div class="card-body p-5">
<form id="formEditAlumni"> <form id="formEditAlumni">
<input type="hidden" id="alumni_id_db" value="{{ $id }}"> <input type="hidden" id="alumni_id_db" value="{{ $id }}">
<div class="row g-4"> <div class="row g-4">
<div class="col-md-12 form-group-custom"> <div class="col-md-12 form-group-custom">
<label>Nama Lengkap Alumni</label> <label>Nama Lengkap Alumni</label>
<input type="text" id="nama_alumni" class="form-control input-custom" required> <input type="text" id="nama_alumni" class="form-control input-custom" required>
</div> </div>
<div class="col-md-12 form-group-custom"> <div class="col-md-12 form-group-custom">
<label>Perguruan Tinggi</label> <label>Perguruan Tinggi</label>
<input type="text" id="perguruan_tinggi" class="form-control input-custom" required> <input type="text" id="perguruan_tinggi" class="form-control input-custom" required>
</div> </div>
<div class="col-md-12 form-group-custom"> <div class="col-md-12 form-group-custom">
<label>Jurusan Kuliah</label> <label>Jurusan Kuliah</label>
<input type="text" id="jurusan_diambil" class="form-control input-custom" required> <input type="text" id="jurusan_diambil" class="form-control input-custom" required>
</div> </div>
</div> </div>
<div class="mt-5 d-flex gap-2 justify-content-end"> <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> <button type="submit" id="btnUpdate" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">
<a href="{{ route('admin.alumni') }}" class="btn btn-light px-4" style="border-radius: 10px;">Batal</a> Perbarui Data
</button>
<a href="{{ route('admin.alumni') }}" class="btn btn-light px-4" style="border-radius: 10px;">
Batal
</a>
</div> </div>
</form> </form>
</div> </div>
@ -40,51 +49,159 @@
</div> </div>
<script> <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() { if (adminIndex > 0) {
try { return origin + '/' + segments.slice(0, adminIndex).join('/');
const response = await fetch(`/api/admin/alumni/${dbId}`, { }
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('access_token') }
}); return origin;
const result = await response.json();
if (response.ok) {
document.getElementById('nama_alumni').value = result.data.nama_alumni;
document.getElementById('perguruan_tinggi').value = result.data.perguruan_tinggi;
document.getElementById('jurusan_diambil').value = result.data.jurusan_diambil;
document.getElementById('display_id_alumni').innerText = `ID Alumni: ${result.data.id_alumni}`;
}
} catch (error) { console.error(error); }
} }
document.getElementById('formEditAlumni').addEventListener('submit', async (e) => { 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(); 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'); const btn = document.getElementById('btnUpdate');
btn.disabled = true; btn.disabled = true;
btn.innerText = 'Memperbarui...';
const payload = { const payload = {
nama_alumni: document.getElementById('nama_alumni').value, nama_alumni: getValue('nama_alumni'),
perguruan_tinggi: document.getElementById('perguruan_tinggi').value, perguruan_tinggi: getValue('perguruan_tinggi'),
jurusan_diambil: document.getElementById('jurusan_diambil').value jurusan_diambil: getValue('jurusan_diambil')
}; };
try { try {
const response = await fetch(`/api/admin/alumni/${dbId}`, { const response = await fetch(`${API_ALUMNI_URL}/${encodeURIComponent(dbId)}`, {
method: 'PUT', method: 'PUT',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('access_token'), 'Authorization': 'Bearer ' + token,
'Accept': 'application/json' 'Accept': 'application/json'
}, },
body: JSON.stringify(payload) body: JSON.stringify(payload)
}); });
if (response.ok) { const result = await parseJsonResponse(response);
Swal.fire({ icon: 'success', title: 'Berhasil Diperbarui!', confirmButtonColor: '#2D6A4F' })
.then(() => { window.location.href = "{{ route('admin.alumni') }}"; }); 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); document.addEventListener('DOMContentLoaded', loadAlumni);

View File

@ -100,11 +100,47 @@
</section> </section>
</div> </div>
@push('scripts')
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<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; 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) { function setText(id, value) {
const element = document.getElementById(id); const element = document.getElementById(id);
@ -164,9 +200,7 @@ function renderDistribusiChart(distribusi) {
const placeholder = document.getElementById('chart-placeholder'); const placeholder = document.getElementById('chart-placeholder');
const canvas = document.getElementById('distribusiChart'); const canvas = document.getElementById('distribusiChart');
if (!placeholder || !canvas) { if (!placeholder || !canvas) return;
return;
}
if (!distribusi || distribusi.length === 0) { if (!distribusi || distribusi.length === 0) {
placeholder.style.display = 'flex'; placeholder.style.display = 'flex';
@ -270,26 +304,32 @@ function renderDistribusiChart(distribusi) {
} }
async function loadDashboardStats() { async function loadDashboardStats() {
const token = localStorage.getItem('access_token'); const token = getToken();
if (!token) { if (!token) {
window.location.href = '/login'; window.location.href = LOGIN_URL;
return; return;
} }
updateLastUpdated(); updateLastUpdated();
try { try {
const response = await fetch('/api/admin/dashboard-stats', { const response = await fetch(API_DASHBOARD_URL, {
method: 'GET',
headers: { headers: {
'Authorization': `Bearer ${token}`, 'Authorization': `Bearer ${token}`,
'Accept': 'application/json' '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.'); throw new Error(result.message || 'Gagal memuat data dashboard.');
} }
@ -306,7 +346,7 @@ function renderDistribusiChart(distribusi) {
} catch (error) { } catch (error) {
console.error('Gagal memuat data dashboard:', 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); updateAccuracyRing(0);
const placeholder = document.getElementById('chart-placeholder'); const placeholder = document.getElementById('chart-placeholder');
@ -325,5 +365,4 @@ function renderDistribusiChart(distribusi) {
document.addEventListener('DOMContentLoaded', loadDashboardStats); document.addEventListener('DOMContentLoaded', loadDashboardStats);
</script> </script>
@endpush
@endsection @endsection

View File

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

View File

@ -7,22 +7,51 @@
<div class="dashboard-section d-flex justify-content-center"> <div class="dashboard-section d-flex justify-content-center">
<div style="width: 100%; max-width: 700px;"> <div style="width: 100%; max-width: 700px;">
<div class="mb-4 text-center"> <div class="mb-4 text-center">
<h4 class="fw-bold text-dark">Tambah Guru BK</h4> <h4 class="fw-bold text-dark">Tambah Guru BK Baru</h4>
<p class="text-muted small">Daftarkan akun guru pembimbing baru untuk sistem rekomendasi.</p> <p class="text-muted small">Daftarkan akun guru pembimbing baru beserta akun login sistem.</p>
</div> </div>
<div class="card border-0 shadow-sm rounded-4"> <div class="card border-0 shadow-sm rounded-4">
<div class="card-body p-5"> <div class="card-body p-5">
<form id="formCreateGuru"> <form id="formCreateGuru">
<div class="row g-4"> <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"> <div class="col-md-12 form-group-custom">
<label class="fw-bold small text-muted">Nama Lengkap & Gelar</label> <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> </div>
<div class="mt-5 d-flex gap-2 justify-content-end"> <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> <button type="submit" id="btnSave" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">
<a href="{{ route('admin.guru') }}" class="btn btn-light px-4" style="border-radius: 10px;">Batal</a> Daftarkan Guru & Akun
</button>
<a href="{{ route('admin.guru') }}" class="btn btn-light px-4" style="border-radius: 10px;">
Batal
</a>
</div> </div>
</form> </form>
</div> </div>
@ -31,32 +60,123 @@
</div> </div>
<script> <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(); 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'); const btn = document.getElementById('btnSave');
btn.disabled = true; btn.disabled = true;
btn.innerText = 'Menyimpan...';
const payload = { 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 { try {
const response = await fetch('/api/admin/guru', { const response = await fetch(API_GURU_URL, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('access_token'), 'Authorization': 'Bearer ' + token,
'Accept': 'application/json' 'Accept': 'application/json'
}, },
body: JSON.stringify(payload) body: JSON.stringify(payload)
}); });
if (response.ok) { const result = await parseJsonResponse(response);
Swal.fire({ icon: 'success', title: 'Berhasil!', confirmButtonColor: '#2D6A4F' })
.then(() => { window.location.href = "{{ route('admin.guru') }}"; }); 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> </script>
@endsection @endsection

View File

@ -15,15 +15,22 @@
<div class="card-body p-5"> <div class="card-body p-5">
<form id="formEditGuru"> <form id="formEditGuru">
<input type="hidden" id="guru_id_db" value="{{ $id }}"> <input type="hidden" id="guru_id_db" value="{{ $id }}">
<div class="row g-4"> <div class="row g-4">
<div class="col-md-12 form-group-custom"> <div class="col-md-12 form-group-custom">
<label class="fw-bold small text-muted">Nama Lengkap & Gelar</label> <label class="fw-bold small text-muted">Nama Lengkap & Gelar</label>
<input type="text" id="nama_guru" class="form-control input-custom" required> <input type="text" id="nama_guru" class="form-control input-custom" required>
</div> </div>
</div> </div>
<div class="mt-5 d-flex gap-2 justify-content-end"> <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> <button type="submit" id="btnUpdate" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">
<a href="{{ route('admin.guru') }}" class="btn btn-light px-4" style="border-radius: 10px;">Batal</a> Update Profil
</button>
<a href="{{ route('admin.guru') }}" class="btn btn-light px-4" style="border-radius: 10px;">
Batal
</a>
</div> </div>
</form> </form>
</div> </div>
@ -32,47 +39,155 @@
</div> </div>
<script> <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() { if (adminIndex > 0) {
try { return origin + '/' + segments.slice(0, adminIndex).join('/');
const response = await fetch(`/api/admin/guru/${dbId}`, { }
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('access_token') }
}); return origin;
const result = await response.json();
if (response.ok) {
document.getElementById('nama_guru').value = result.data.nama_guru;
document.getElementById('display_id_guru').innerText = `Ubah profil administrator guru BK (${result.data.id_guru}).`;
}
} catch (error) { console.error(error); }
} }
document.getElementById('formEditGuru').addEventListener('submit', async (e) => { 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(); 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'); const btn = document.getElementById('btnUpdate');
btn.disabled = true; btn.disabled = true;
btn.innerText = 'Memperbarui...';
const payload = { const payload = {
nama_guru: document.getElementById('nama_guru').value nama_guru: getValue('nama_guru')
}; };
try { try {
const response = await fetch(`/api/admin/guru/${dbId}`, { const response = await fetch(`${API_GURU_URL}/${encodeURIComponent(dbId)}`, {
method: 'PUT', method: 'PUT',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('access_token'), 'Authorization': 'Bearer ' + token,
'Accept': 'application/json' 'Accept': 'application/json'
}, },
body: JSON.stringify(payload) body: JSON.stringify(payload)
}); });
if (response.ok) { const result = await parseJsonResponse(response);
Swal.fire({ icon: 'success', title: 'Berhasil Diperbarui!', confirmButtonColor: '#2D6A4F' })
.then(() => { window.location.href = "{{ route('admin.guru') }}"; }); 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); document.addEventListener('DOMContentLoaded', loadGuru);

View File

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

View File

@ -19,6 +19,7 @@
<label class="fw-bold small text-muted">Nama Jurusan</label> <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> <input type="text" id="nama_jurusan" class="form-control input-custom" placeholder="Contoh: Teknik Elektro" required>
</div> </div>
<div class="col-md-12 form-group-custom"> <div class="col-md-12 form-group-custom">
<label class="fw-bold small text-muted">Rumpun</label> <label class="fw-bold small text-muted">Rumpun</label>
<select id="rumpun" class="form-select input-custom" required> <select id="rumpun" class="form-select input-custom" required>
@ -27,14 +28,16 @@
<option value="Soshum">Soshum</option> <option value="Soshum">Soshum</option>
</select> </select>
</div> </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"> <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> <button type="submit" id="btnSave" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">
<a href="{{ route('admin.jurusan') }}" class="btn btn-light px-4" style="border-radius: 10px;">Batal</a> Simpan Data
</button>
<a href="{{ route('admin.jurusan') }}" class="btn btn-light px-4" style="border-radius: 10px;">
Batal
</a>
</div> </div>
</form> </form>
</div> </div>
@ -43,34 +46,119 @@
</div> </div>
<script> <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(); 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'); const btn = document.getElementById('btnSave');
btn.disabled = true; btn.disabled = true;
btn.innerText = 'Menyimpan...';
const payload = { const payload = {
nama_jurusan: document.getElementById('nama_jurusan').value, nama_jurusan: getValue('nama_jurusan'),
rumpun: document.getElementById('rumpun').value, rumpun: getValue('rumpun'),
prospek_karir: document.getElementById('prospek_karir').value
}; };
try { try {
const response = await fetch('/api/admin/jurusan', { const response = await fetch(API_JURUSAN_URL, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('access_token'), 'Authorization': 'Bearer ' + token,
'Accept': 'application/json' 'Accept': 'application/json'
}, },
body: JSON.stringify(payload) body: JSON.stringify(payload)
}); });
if (response.ok) { const result = await parseJsonResponse(response);
Swal.fire({ icon: 'success', title: 'Berhasil!', confirmButtonColor: '#2D6A4F' })
.then(() => { window.location.href = "{{ route('admin.jurusan') }}"; }); 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> </script>
@endsection @endsection

View File

@ -15,11 +15,13 @@
<div class="card-body p-5"> <div class="card-body p-5">
<form id="formEditJurusan"> <form id="formEditJurusan">
<input type="hidden" id="jurusan_id" value="{{ $id }}"> <input type="hidden" id="jurusan_id" value="{{ $id }}">
<div class="row g-4"> <div class="row g-4">
<div class="col-md-12 form-group-custom"> <div class="col-md-12 form-group-custom">
<label class="fw-bold small text-muted">Nama Jurusan</label> <label class="fw-bold small text-muted">Nama Jurusan</label>
<input type="text" id="nama_jurusan" class="form-control input-custom" required> <input type="text" id="nama_jurusan" class="form-control input-custom" required>
</div> </div>
<div class="col-md-12 form-group-custom"> <div class="col-md-12 form-group-custom">
<label class="fw-bold small text-muted">Rumpun</label> <label class="fw-bold small text-muted">Rumpun</label>
<select id="rumpun" class="form-select input-custom" required> <select id="rumpun" class="form-select input-custom" required>
@ -27,14 +29,16 @@
<option value="Soshum">Soshum</option> <option value="Soshum">Soshum</option>
</select> </select>
</div> </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"> <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> <button type="submit" id="btnUpdate" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">
<a href="{{ route('admin.jurusan') }}" class="btn btn-light px-4" style="border-radius: 10px;">Batal</a> Perbarui Data
</button>
<a href="{{ route('admin.jurusan') }}" class="btn btn-light px-4" style="border-radius: 10px;">
Batal
</a>
</div> </div>
</form> </form>
</div> </div>
@ -43,50 +47,156 @@
</div> </div>
<script> <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() { if (adminIndex > 0) {
try { return origin + '/' + segments.slice(0, adminIndex).join('/');
const response = await fetch(`/api/admin/jurusan/${jurusanId}`, { }
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('access_token') }
}); return origin;
const result = await response.json();
if (response.ok) {
document.getElementById('nama_jurusan').value = result.data.nama_jurusan;
document.getElementById('rumpun').value = result.data.rumpun;
document.getElementById('prospek_karir').value = result.data.prospek_karir || '';
}
} catch (error) { console.error(error); }
} }
document.getElementById('formEditJurusan').addEventListener('submit', async (e) => { 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(); 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'); const btn = document.getElementById('btnUpdate');
btn.disabled = true; btn.disabled = true;
btn.innerText = 'Memperbarui...';
const payload = { const payload = {
nama_jurusan: document.getElementById('nama_jurusan').value, nama_jurusan: getValue('nama_jurusan'),
rumpun: document.getElementById('rumpun').value, rumpun: getValue('rumpun'),
prospek_karir: document.getElementById('prospek_karir').value
}; };
try { try {
const response = await fetch(`/api/admin/jurusan/${jurusanId}`, { const response = await fetch(`${API_JURUSAN_URL}/${encodeURIComponent(jurusanId)}`, {
method: 'PUT', method: 'PUT',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('access_token'), 'Authorization': 'Bearer ' + token,
'Accept': 'application/json' 'Accept': 'application/json'
}, },
body: JSON.stringify(payload) body: JSON.stringify(payload)
}); });
if (response.ok) { const result = await parseJsonResponse(response);
Swal.fire({ icon: 'success', title: 'Berhasil Diperbarui!', confirmButtonColor: '#2D6A4F' })
.then(() => { window.location.href = "{{ route('admin.jurusan') }}"; }); 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); document.addEventListener('DOMContentLoaded', loadData);

View File

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

View File

@ -21,22 +21,31 @@
<option value="" disabled selected>Memuat daftar jurusan...</option> <option value="" disabled selected>Memuat daftar jurusan...</option>
</select> </select>
</div> </div>
<div class="col-md-12 form-group-custom"> <div class="col-md-12 form-group-custom">
<label class="fw-bold small text-muted">Nama Profesi</label> <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> <input type="text" id="nama_profesi" class="form-control input-custom" placeholder="Contoh: Arsitek Profesional" required>
</div> </div>
<div class="col-md-12 form-group-custom"> <div class="col-md-12 form-group-custom">
<label class="fw-bold small text-muted">Range Gaji (Opsional)</label> <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"> <input type="text" id="range_gaji" class="form-control input-custom" placeholder="Contoh: Rp 6.000.000 - Rp 15.000.000">
</div> </div>
<div class="col-md-12 form-group-custom"> <div class="col-md-12 form-group-custom">
<label class="fw-bold small text-muted">Deskripsi Pekerjaan (Opsional)</label> <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> <textarea id="deskripsi_pekerjaan" class="form-control input-custom" rows="3" placeholder="Jelaskan peran pekerjaan ini secara singkat..."></textarea>
</div> </div>
</div> </div>
<div class="mt-5 d-flex gap-2 justify-content-end"> <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> <button type="submit" id="btnSave" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">
<a href="{{ route('admin.prospek-kerja') }}" class="btn btn-light px-4" style="border-radius: 10px;">Batal</a> Simpan Data
</button>
<a href="{{ route('admin.prospek-kerja') }}" class="btn btn-light px-4" style="border-radius: 10px;">
Batal
</a>
</div> </div>
</form> </form>
</div> </div>
@ -45,54 +54,180 @@
</div> </div>
<script> <script>
async function loadJurusan() { function getAppBaseUrl() {
try { const origin = window.location.origin;
const response = await fetch('/api/admin/jurusan', { const segments = window.location.pathname.split('/').filter(Boolean);
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('access_token') } const adminIndex = segments.indexOf('admin');
});
const result = await response.json();
const select = document.getElementById('jurusan_id');
select.innerHTML = '<option value="" disabled selected>Pilih Jurusan Terkait...</option>';
result.data.forEach(item => { if (adminIndex > 0) {
select.innerHTML += `<option value="${item.id}">${item.nama_jurusan}</option>`; 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) { } 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(); 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'); const btn = document.getElementById('btnSave');
btn.disabled = true; btn.disabled = true;
btn.innerText = 'Menyimpan...';
const payload = { const payload = {
jurusan_id: document.getElementById('jurusan_id').value, jurusan_id: getValue('jurusan_id'),
nama_profesi: document.getElementById('nama_profesi').value, nama_profesi: getValue('nama_profesi'),
range_gaji: document.getElementById('range_gaji').value, range_gaji: getValue('range_gaji'),
deskripsi_pekerjaan: document.getElementById('deskripsi_pekerjaan').value deskripsi_pekerjaan: getValue('deskripsi_pekerjaan')
}; };
try { try {
const response = await fetch('/api/admin/prospek-kerja', { const response = await fetch(API_PROSPEK_URL, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('access_token'), 'Authorization': 'Bearer ' + token,
'Accept': 'application/json' 'Accept': 'application/json'
}, },
body: JSON.stringify(payload) body: JSON.stringify(payload)
}); });
if (response.ok) { const result = await parseJsonResponse(response);
Swal.fire({ icon: 'success', title: 'Berhasil!', confirmButtonColor: '#2D6A4F' })
.then(() => { window.location.href = "{{ route('admin.prospek-kerja') }}"; }); if (response.status === 401 || response.status === 403) {
} else { Swal.fire('Sesi Berakhir', 'Silakan login ulang.', 'warning')
Swal.fire('Error!', 'Periksa kembali isian Anda.', 'error'); .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); document.addEventListener('DOMContentLoaded', loadJurusan);

View File

@ -15,6 +15,7 @@
<div class="card-body p-5"> <div class="card-body p-5">
<form id="formEditProspek"> <form id="formEditProspek">
<input type="hidden" id="prospek_id" value="{{ $id }}"> <input type="hidden" id="prospek_id" value="{{ $id }}">
<div class="row g-4"> <div class="row g-4">
<div class="col-md-12 form-group-custom"> <div class="col-md-12 form-group-custom">
<label class="fw-bold small text-muted">Kaitkan dengan Jurusan</label> <label class="fw-bold small text-muted">Kaitkan dengan Jurusan</label>
@ -22,22 +23,31 @@
<option value="" disabled>Memuat daftar jurusan...</option> <option value="" disabled>Memuat daftar jurusan...</option>
</select> </select>
</div> </div>
<div class="col-md-12 form-group-custom"> <div class="col-md-12 form-group-custom">
<label class="fw-bold small text-muted">Nama Profesi</label> <label class="fw-bold small text-muted">Nama Profesi</label>
<input type="text" id="nama_profesi" class="form-control input-custom" required> <input type="text" id="nama_profesi" class="form-control input-custom" required>
</div> </div>
<div class="col-md-12 form-group-custom"> <div class="col-md-12 form-group-custom">
<label class="fw-bold small text-muted">Range Gaji</label> <label class="fw-bold small text-muted">Range Gaji</label>
<input type="text" id="range_gaji" class="form-control input-custom"> <input type="text" id="range_gaji" class="form-control input-custom">
</div> </div>
<div class="col-md-12 form-group-custom"> <div class="col-md-12 form-group-custom">
<label class="fw-bold small text-muted">Deskripsi Pekerjaan</label> <label class="fw-bold small text-muted">Deskripsi Pekerjaan</label>
<textarea id="deskripsi_pekerjaan" class="form-control input-custom" rows="3"></textarea> <textarea id="deskripsi_pekerjaan" class="form-control input-custom" rows="3"></textarea>
</div> </div>
</div> </div>
<div class="mt-5 d-flex gap-2 justify-content-end"> <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> <button type="submit" id="btnUpdate" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">
<a href="{{ route('admin.prospek-kerja') }}" class="btn btn-light px-4" style="border-radius: 10px;">Batal</a> Perbarui Data
</button>
<a href="{{ route('admin.prospek-kerja') }}" class="btn btn-light px-4" style="border-radius: 10px;">
Batal
</a>
</div> </div>
</form> </form>
</div> </div>
@ -46,66 +56,212 @@
</div> </div>
<script> <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() { if (adminIndex > 0) {
const token = localStorage.getItem('access_token'); return origin + '/' + segments.slice(0, adminIndex).join('/');
}
try { return origin;
const resJurusan = await fetch('/api/admin/jurusan', {
headers: { 'Authorization': 'Bearer ' + token }
});
const resultJurusan = await resJurusan.json();
const select = document.getElementById('jurusan_id');
select.innerHTML = '<option value="" disabled>Pilih Jurusan Terkait...</option>';
resultJurusan.data.forEach(item => {
select.innerHTML += `<option value="${item.id}">${item.nama_jurusan}</option>`;
});
const resProspek = await fetch(`/api/admin/prospek-kerja/${prospekId}`, {
headers: { 'Authorization': 'Bearer ' + token }
});
const resultProspek = await resProspek.json();
if (resProspek.ok) {
document.getElementById('jurusan_id').value = resultProspek.data.jurusan_id;
document.getElementById('nama_profesi').value = resultProspek.data.nama_profesi;
document.getElementById('range_gaji').value = resultProspek.data.range_gaji || '';
document.getElementById('deskripsi_pekerjaan').value = resultProspek.data.deskripsi_pekerjaan || '';
}
} catch (error) { console.error(error); }
} }
document.getElementById('formEditProspek').addEventListener('submit', async (e) => { 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(); 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'); const btn = document.getElementById('btnUpdate');
btn.disabled = true; btn.disabled = true;
btn.innerText = 'Memperbarui...';
const payload = { const payload = {
jurusan_id: document.getElementById('jurusan_id').value, jurusan_id: getValue('jurusan_id'),
nama_profesi: document.getElementById('nama_profesi').value, nama_profesi: getValue('nama_profesi'),
range_gaji: document.getElementById('range_gaji').value, range_gaji: getValue('range_gaji'),
deskripsi_pekerjaan: document.getElementById('deskripsi_pekerjaan').value deskripsi_pekerjaan: getValue('deskripsi_pekerjaan')
}; };
try { try {
const response = await fetch(`/api/admin/prospek-kerja/${prospekId}`, { const response = await fetch(`${API_PROSPEK_URL}/${encodeURIComponent(prospekId)}`, {
method: 'PUT', method: 'PUT',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('access_token'), 'Authorization': 'Bearer ' + token,
'Accept': 'application/json' 'Accept': 'application/json'
}, },
body: JSON.stringify(payload) body: JSON.stringify(payload)
}); });
if (response.ok) { const result = await parseJsonResponse(response);
Swal.fire({ icon: 'success', title: 'Berhasil Diperbarui!', confirmButtonColor: '#2D6A4F' })
.then(() => { window.location.href = "{{ route('admin.prospek-kerja') }}"; }); 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); document.addEventListener('DOMContentLoaded', loadJurusanAndData);

View File

@ -94,13 +94,36 @@
</div> </div>
</div> </div>
@push('scripts')
<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_RIWAYAT_URL = `${APP_BASE_URL}/api/admin/riwayat`;
const LOGIN_URL = `${APP_BASE_URL}/login`;
let currentPage = 1; let currentPage = 1;
let currentSearch = ''; let currentSearch = '';
let perPage = 7; let perPage = 7;
let searchTimer = null; let searchTimer = null;
function getToken() {
return localStorage.getItem('access_token') ||
localStorage.getItem('token') ||
sessionStorage.getItem('access_token') ||
sessionStorage.getItem('token') ||
'';
}
function escapeHtml(value) { function escapeHtml(value) {
if (value === null || value === undefined || value === '') return '-'; if (value === null || value === undefined || value === '') return '-';
@ -112,6 +135,19 @@ function escapeHtml(value) {
.replaceAll("'", '&#039;'); .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) { function formatPercent(value) {
if (value === null || value === undefined || value === '') { if (value === null || value === undefined || value === '') {
return '-'; return '-';
@ -152,11 +188,11 @@ function showLoading() {
} }
async function fetchRiwayat(page = 1) { async function fetchRiwayat(page = 1) {
const token = localStorage.getItem('access_token'); const token = getToken();
const tbody = document.getElementById('table-riwayat'); const tbody = document.getElementById('table-riwayat');
if (!token) { if (!token) {
window.location.href = '/login'; window.location.href = LOGIN_URL;
return; return;
} }
@ -173,14 +209,20 @@ function showLoading() {
} }
try { try {
const response = await fetch(`/api/admin/riwayat?${params.toString()}`, { const response = await fetch(`${API_RIWAYAT_URL}?${params.toString()}`, {
method: 'GET',
headers: { headers: {
'Authorization': `Bearer ${token}`, 'Authorization': `Bearer ${token}`,
'Accept': 'application/json' '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) { if (!response.ok || result.success === false) {
throw new Error(result.message || 'Gagal memuat data riwayat.'); throw new Error(result.message || 'Gagal memuat data riwayat.');
@ -219,6 +261,7 @@ function showLoading() {
<td> <td>
<div class="riwayat-user-cell"> <div class="riwayat-user-cell">
<div class="riwayat-avatar">${escapeHtml(getInitial(item.nama_siswa))}</div> <div class="riwayat-avatar">${escapeHtml(getInitial(item.nama_siswa))}</div>
<div> <div>
<div class="riwayat-user-name">${escapeHtml(item.nama_siswa)}</div> <div class="riwayat-user-name">${escapeHtml(item.nama_siswa)}</div>
<div class="riwayat-user-email">${escapeHtml(item.email_siswa)}</div> <div class="riwayat-user-email">${escapeHtml(item.email_siswa)}</div>
@ -245,7 +288,7 @@ function showLoading() {
</td> </td>
<td class="text-center"> <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 <i class="fas fa-file-alt me-1"></i> Detail
</button> </button>
</td> </td>
@ -261,7 +304,7 @@ function showLoading() {
<tr> <tr>
<td colspan="7" class="empty-state-modern text-danger"> <td colspan="7" class="empty-state-modern text-danger">
<i class="fas fa-exclamation-circle"></i> <i class="fas fa-exclamation-circle"></i>
Gagal memuat data riwayat. ${escapeHtml(error.message || 'Gagal memuat data riwayat.')}
</td> </td>
</tr> </tr>
`; `;
@ -307,18 +350,26 @@ function renderPagination(data) {
let pages = []; let pages = [];
if (last <= 5) { if (last <= 5) {
for (let i = 1; i <= last; i++) pages.push(i); for (let i = 1; i <= last; i++) {
pages.push(i);
}
} else { } else {
pages.push(1); pages.push(1);
if (current > 3) pages.push('...'); if (current > 3) {
pages.push('...');
}
const start = Math.max(2, current - 1); const start = Math.max(2, current - 1);
const end = Math.min(last - 1, 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); pages.push(last);
} }
@ -348,9 +399,14 @@ function renderObjectGrid(data, className) {
} }
async function showDetail(id) { async function showDetail(id) {
const token = localStorage.getItem('access_token'); const token = getToken();
const modalBody = document.getElementById('modal-body-content'); const modalBody = document.getElementById('modal-body-content');
if (!token) {
window.location.href = LOGIN_URL;
return;
}
const detailModal = new bootstrap.Modal(document.getElementById('detailModal')); const detailModal = new bootstrap.Modal(document.getElementById('detailModal'));
detailModal.show(); detailModal.show();
@ -362,76 +418,86 @@ function renderObjectGrid(data, className) {
`; `;
try { try {
const response = await fetch(`/api/admin/riwayat/${id}`, { const response = await fetch(`${API_RIWAYAT_URL}/${encodeURIComponent(id)}`, {
method: 'GET',
headers: { headers: {
'Authorization': `Bearer ${token}`, 'Authorization': `Bearer ${token}`,
'Accept': 'application/json' 'Accept': 'application/json'
} }
}); });
const result = await response.json(); const result = await parseJsonResponse(response);
if (result.success) { if (response.status === 401 || response.status === 403) {
const data = result.data; window.location.href = LOGIN_URL;
return;
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.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) { } 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> </script>
@endpush
@endsection @endsection

View File

@ -21,271 +21,187 @@
<div class="table-toolbar"> <div class="table-toolbar">
<div class="search-wrapper-modern"> <div class="search-wrapper-modern">
<i class="fas fa-search"></i> <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>
<div class="per-page-wrapper"> <div class="per-page-wrapper">
<span>Tampilkan</span> <span>Tampilkan</span>
<select id="perPageSelect" class="per-page-select"> <select id="perPageSelect" class="per-page-select">
<option value="7" selected>7 data</option>
<option value="5">5 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> </select>
</div> </div>
</div> </div>
<div class="table-responsive"> <div id="tableContent">
<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> </div>
<div class="d-flex gap-1 flex-wrap" id="pagination-controls"></div>
</div>
</div> </div>
</div> </div>
<script> <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 currentPage = 1;
let currentSearch = ''; let currentSearch = '';
let perPage = 7; let perPage = 7;
let searchTimer = null; let searchTimer;
function escapeHtml(value) {
if (value === null || value === undefined || value === '') return '-';
return String(value)
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#039;');
}
function getInitial(name) {
const cleanName = name || 'S';
return cleanName.charAt(0).toUpperCase();
}
function showLoading() {
document.getElementById('siswa-table-body').innerHTML = `
<tr>
<td colspan="6" class="loading-state-modern">
<i class="fas fa-spinner fa-spin me-2"></i> Memuat data siswa...
</td>
</tr>
`;
}
async function fetchSiswa(page = 1) { 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; currentPage = page;
showLoading(); 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>';
const params = new URLSearchParams({
page: page,
per_page: perPage
});
if (currentSearch !== '') {
params.append('search', currentSearch);
}
try { try {
const response = await fetch(`/api/admin/siswa?${params.toString()}`, { const token = getToken();
method: 'GET', const response = await fetch(`${API_SISWA_URL}?page=${page}&per_page=${perPage}&search=${encodeURIComponent(currentSearch)}`, {
headers: { headers: {
'Authorization': 'Bearer ' + token, 'Authorization': 'Bearer ' + token,
'Accept': 'application/json' 'Accept': 'application/json'
} }
}); });
const data = await parseJsonResponse(response);
const result = await response.json(); if (response.ok && data.success !== false) {
renderTable(data.data.data, data.data.current_page, data.data.per_page);
if (!response.ok || result.success === false) { renderPagination(data.data);
throw new Error(result.message || 'Gagal memuat data siswa.'); } 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) { } catch (error) {
tbody.innerHTML = ` 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>`;
<tr>
<td colspan="6" class="empty-state-modern text-danger">
<i class="fas fa-exclamation-circle"></i>
Gagal memuat data siswa.
</td>
</tr>
`;
} }
} }
function updatePaginationInfo(data) { function renderTable(siswaData, currentPage, perPage) {
const info = document.getElementById('pagination-info'); const tableContent = document.getElementById('tableContent');
if (!data || !data.total) { if (siswaData.length === 0) {
info.innerText = 'Menampilkan 0 sampai 0 dari 0 data'; 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; 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) { siswaData.forEach((siswa, index) => {
const controls = document.getElementById('pagination-controls'); const no = (currentPage - 1) * perPage + (index + 1);
controls.innerHTML = ''; 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); html += `
const last = Number(data.last_page || 1); <tr>
const total = Number(data.total || 0); <td class="text-center">${no}</td>
<td class="fw-bold">${siswa.nama_lengkap || '-'}</td>
if (total === 0) return; <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>
const createButton = (label, page, disabled = false, active = false) => { <td>${username}</td>
const btn = document.createElement('button'); <td class="text-center">
btn.className = `page-link-custom ${active ? 'active' : ''}`; <div class="d-flex justify-content-center gap-2">
btn.innerHTML = label; <a href="${editUrl}" class="btn btn-sm btn-outline-primary shadow-sm" title="Edit Data">
btn.disabled = disabled; <i class="fas fa-edit"></i>
btn.onclick = () => { </a>
if (!disabled && !active) fetchSiswa(page); <button onclick="deleteSiswa(${siswa.id})" class="btn btn-sm btn-outline-danger shadow-sm" title="Hapus Data">
}; <i class="fas fa-trash"></i>
return btn; </button>
}; </div>
</td>
controls.appendChild(createButton('<i class="fas fa-chevron-left"></i>', current - 1, current === 1)); </tr>
`;
let pages = [];
if (last <= 5) {
for (let i = 1; i <= last; i++) pages.push(i);
} else {
pages.push(1);
if (current > 3) pages.push('...');
const start = Math.max(2, current - 1);
const end = Math.min(last - 1, current + 1);
for (let i = start; i <= end; i++) pages.push(i);
if (current < last - 2) pages.push('...');
pages.push(last);
}
pages.forEach(page => {
if (page === '...') {
controls.appendChild(createButton('...', current, true));
} else {
controls.appendChild(createButton(page, page, false, page === current));
}
}); });
controls.appendChild(createButton('<i class="fas fa-chevron-right"></i>', current + 1, current === last)); 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) { async function deleteSiswa(id) {
const result = await Swal.fire({ const confirmResult = await Swal.fire({
title: 'Hapus data?', title: 'Hapus data?',
text: 'Data siswa akan dihapus permanen!', text: "Data yang dihapus tidak dapat dikembalikan!",
icon: 'warning', icon: 'warning',
showCancelButton: true, showCancelButton: true,
confirmButtonColor: '#ef4444', confirmButtonColor: '#d33',
cancelButtonColor: '#64748b', cancelButtonColor: '#3085d6',
confirmButtonText: 'Ya, Hapus', confirmButtonText: 'Ya, Hapus!',
cancelButtonText: 'Batal' cancelButtonText: 'Batal'
}); });
if (!result.isConfirmed) return; if (!confirmResult.isConfirmed) return;
const token = localStorage.getItem('access_token');
const token = getToken();
try { try {
const response = await fetch('/api/admin/siswa/' + id, { const response = await fetch(`${API_SISWA_URL}/${id}`, {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
'Authorization': 'Bearer ' + token, 'Authorization': 'Bearer ' + token,
'Accept': 'application/json' 'Accept': 'application/json'
} }
}); });
const resultDelete = await parseJsonResponse(response);
if (response.ok) { if (response.ok && resultDelete.success !== false) {
Swal.fire('Berhasil!', 'Data siswa berhasil dihapus.', 'success'); Swal.fire('Terhapus!', resultDelete.message || 'Data berhasil dihapus.', 'success');
fetchSiswa(currentPage); fetchSiswa(currentPage);
} else { } else {
Swal.fire('Gagal!', 'Data siswa gagal dihapus.', 'error'); Swal.fire('Gagal!', resultDelete.message || 'Gagal menghapus data.', 'error');
} }
} catch (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(() => { searchTimer = setTimeout(() => {
currentSearch = this.value.trim(); currentSearch = this.value.trim();
fetchSiswa(1); fetchSiswa(1);
}, 350); }, 500);
}); });
document.getElementById('perPageSelect').addEventListener('change', function () { document.getElementById('perPageSelect').addEventListener('change', function () {
perPage = this.value; perPage = parseInt(this.value);
fetchSiswa(1); fetchSiswa(1);
}); });
}); });

View File

@ -7,52 +7,73 @@
<div class="dashboard-section d-flex justify-content-center"> <div class="dashboard-section d-flex justify-content-center">
<div style="width: 100%; max-width: 700px;"> <div style="width: 100%; max-width: 700px;">
<div class="mb-4 text-center"> <div class="mb-4 text-center">
<h4 class="fw-bold text-dark">Tambah Siswa Baru</h4> <h4 class="fw-bold text-dark">Tambah Data Siswa Baru</h4>
<p class="text-muted small text-uppercase fw-bold" style="letter-spacing: 1px;">Sistem Rekomendasi SMAN 4 Jember</p> <p class="text-muted small">Isi data di bawah ini untuk membuat profil siswa sekaligus akun login sistem.</p>
</div> </div>
<div class="card border-0 shadow-sm rounded-4"> <div class="card border-0 shadow-sm rounded-4">
<div class="card-body p-5"> <div class="card-body p-5">
<form id="createSiswaForm"> <form id="formCreateSiswa">
<div class="row g-4"> <div class="row g-4">
<div class="col-md-12 form-group-custom"> <div class="col-12">
<label>Nama Lengkap</label> <div class="text-muted small fw-bold text-uppercase mb-2" style="letter-spacing: 1px;">
<input type="text" id="nama_lengkap" class="form-control input-custom" required placeholder="Masukkan nama lengkap siswa"> <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>
<div class="col-md-12 form-group-custom"> <div class="col-md-12 form-group-custom">
<label>NISN</label> <label class="fw-bold small text-muted">Nama Lengkap</label>
<input type="text" id="nisn" class="form-control input-custom" required placeholder="Masukkan 10 digit NISN"> <input type="text" id="nama_lengkap" class="form-control input-custom" placeholder="Masukkan nama sesuai ijazah/rapor" required>
</div> </div>
<div class="col-md-12 form-group-custom"> <div class="col-md-6 form-group-custom">
<label>Kelas</label> <label class="fw-bold small text-muted">NISN</label>
<input type="text" id="kelas" class="form-control input-custom" required placeholder="Contoh: XII IPA 1"> <input type="text" id="nisn" class="form-control input-custom" placeholder="10 Digit nomor NISN" maxlength="10" required>
</div> </div>
<div class="col-md-12 form-group-custom"> <div class="col-md-6 form-group-custom">
<label>Peminatan</label> <label class="fw-bold small text-muted">Kelas</label>
<select id="peminatan" class="form-select input-custom"> <select id="kelas" class="form-select input-custom" required>
<option value="" disabled selected>Pilih Peminatan</option> <option value="" disabled selected>Pilih Kelas...</option>
<option value="Saintek">Saintek</option> <option value="XII MIPA 1">XII MIPA 1</option>
<option value="Soshum">Soshum</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> </select>
</div> </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>
<div class="mt-5 d-flex gap-2 justify-content-end"> <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> <button type="submit" id="btnSave" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">
<a href="{{ route('admin.siswa') }}" class="btn btn-light px-4" style="border-radius: 10px;">Batal</a> Simpan Data
</button>
<a href="{{ route('admin.siswa') }}" class="btn btn-light px-4" style="border-radius: 10px;">
Batal
</a>
</div> </div>
</form> </form>
</div> </div>
@ -61,43 +82,109 @@
</div> </div>
<script> <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(); 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'); const btn = document.getElementById('btnSave');
btn.disabled = true; btn.disabled = true;
btn.innerText = 'Menyimpan...';
const payload = { const payload = {
user_id: document.getElementById('user_id').value, username: getValue('username'),
jurusan_id: document.getElementById('jurusan_id').value, password: getValue('password'),
nama_lengkap: document.getElementById('nama_lengkap').value, nama_lengkap: getValue('nama_lengkap'),
nisn: document.getElementById('nisn').value, nisn: getValue('nisn'),
kelas: document.getElementById('kelas').value, kelas: getValue('kelas')
peminatan: document.getElementById('peminatan').value,
}; };
try { try {
const response = await fetch('/api/admin/siswa', { const response = await fetch(API_SISWA_URL, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('access_token'), 'Authorization': 'Bearer ' + token,
'Accept': 'application/json' 'Accept': 'application/json'
}, },
body: JSON.stringify(payload) body: JSON.stringify(payload)
}); });
const result = await response.json(); const result = await parseJsonResponse(response);
if (response.ok) { if (response.status === 401 || response.status === 403) {
Swal.fire({ icon: 'success', title: 'Berhasil!', text: 'Data siswa ditambahkan.', confirmButtonColor: '#2D7A47' }) Swal.fire('Sesi Berakhir', 'Silakan login ulang.', 'warning')
.then(() => { window.location.href = "{{ route('admin.siswa') }}"; }); .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 { } 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) { } catch (error) {
Swal.fire('Error!', 'Gagal menghubungi server.', 'error'); Swal.fire('Error!', error.message || 'Gagal menghubungi server.', 'error');
} finally { } finally {
btn.disabled = false; btn.disabled = false;
btn.innerText = 'Simpan Data';
} }
}); });
</script> </script>

View File

@ -7,43 +7,76 @@
<div class="dashboard-section d-flex justify-content-center"> <div class="dashboard-section d-flex justify-content-center">
<div style="width: 100%; max-width: 700px;"> <div style="width: 100%; max-width: 700px;">
<div class="mb-4 text-center"> <div class="mb-4 text-center">
<h4 class="fw-bold text-dark">Edit Data Siswa</h4> <h4 class="fw-bold text-dark">Edit Data Profil Siswa</h4>
<p id="display_id_siswa" class="text-muted small fw-bold">Memuat data...</p> <p class="text-muted small">Ubah rincian informasi dan akun profil siswa.</p>
</div> </div>
<div class="card border-0 shadow-sm rounded-4"> <div class="card border-0 shadow-sm rounded-4">
<div class="card-body p-5"> <div class="card-body p-5">
<form id="editSiswaForm"> <form id="formEditSiswa">
<input type="hidden" id="siswa_id_db" value="{{ $id }}"> <input type="hidden" id="siswa_id" value="{{ $id }}">
<div class="row g-4"> <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"> <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> <input type="text" id="nama_lengkap" class="form-control input-custom" required>
</div> </div>
<div class="col-md-12 form-group-custom"> <div class="col-md-6 form-group-custom">
<label>NISN</label> <label class="fw-bold small text-muted">NISN</label>
<input type="text" id="nisn" class="form-control input-custom" required> <input type="text" id="nisn" class="form-control input-custom" maxlength="10" required>
</div> </div>
<div class="col-md-12 form-group-custom"> <div class="col-md-6 form-group-custom">
<label>Kelas</label> <label class="fw-bold small text-muted">Kelas</label>
<input type="text" id="kelas" class="form-control input-custom" required> <select id="kelas" class="form-select input-custom" required>
</div> <option value="" disabled selected>Pilih Kelas...</option>
<option value="XII MIPA 1">XII MIPA 1</option>
<div class="col-md-12 form-group-custom"> <option value="XII MIPA 2">XII MIPA 2</option>
<label>Peminatan</label> <option value="XII MIPA 3">XII MIPA 3</option>
<select id="peminatan" class="form-select input-custom"> <option value="XII MIPA 4">XII MIPA 4</option>
<option value="Saintek">Saintek</option> <option value="XII IPS 1">XII IPS 1</option>
<option value="Soshum">Soshum</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> </select>
</div> </div>
</div> </div>
<div class="mt-5 d-flex gap-2 justify-content-end"> <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> <button type="submit" id="btnUpdate" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">
<a href="{{ route('admin.siswa') }}" class="btn btn-light px-4" style="border-radius: 10px;">Batal</a> Perbarui Data
</button>
<a href="{{ route('admin.siswa') }}" class="btn btn-light px-4" style="border-radius: 10px;">
Batal
</a>
</div> </div>
</form> </form>
</div> </div>
@ -52,71 +85,134 @@
</div> </div>
<script> <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 const APP_BASE_URL = getAppBaseUrl();
async function loadSiswa() { 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 { try {
const response = await fetch(`/api/admin/siswa/${dbId}`, { return text ? JSON.parse(text) : {};
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('access_token'),
'Accept': 'application/json'
}
});
const result = await response.json();
if (response.ok) {
document.getElementById('nama_lengkap').value = result.data.nama_lengkap;
document.getElementById('nisn').value = result.data.nisn;
document.getElementById('kelas').value = result.data.kelas;
document.getElementById('peminatan').value = result.data.peminatan;
document.getElementById('display_id_siswa').innerText = `EDITING: ${result.data.id_siswa}`;
} else {
Swal.fire('Gagal!', 'Data tidak ditemukan.', 'warning');
}
} catch (error) { } 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.addEventListener('DOMContentLoaded', async function () {
document.getElementById('editSiswaForm').addEventListener('submit', async (e) => { const id = document.getElementById('siswa_id').value;
e.preventDefault(); const token = getToken();
const btn = document.getElementById('btnUpdate');
btn.disabled = true;
const payload = { if (!token) {
nama_lengkap: document.getElementById('nama_lengkap').value, window.location.href = LOGIN_URL;
nisn: document.getElementById('nisn').value, return;
kelas: document.getElementById('kelas').value, }
peminatan: document.getElementById('peminatan').value
};
try { 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', method: 'PUT',
headers: { headers: {
'Authorization': 'Bearer ' + getToken(),
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('access_token'),
'Accept': 'application/json' 'Accept': 'application/json'
}, },
body: JSON.stringify(payload) body: JSON.stringify(payload)
}); });
if (response.ok) { const result = await parseJsonResponse(response);
Swal.fire({ icon: 'success', title: 'Berhasil Diperbarui!', text: 'Data siswa telah diperbarui.', confirmButtonColor: '#2D7A47' })
.then(() => { window.location.href = "{{ route('admin.siswa') }}"; }); 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 { } else {
const result = await response.json(); Swal.fire({
Swal.fire('Gagal!', result.message || 'Cek inputan.', 'warning'); icon: 'warning',
title: 'Gagal!',
text: result.message || 'Cek kembali rincian data yang Anda ubah.',
confirmButtonColor: '#2D7A47'
});
} }
} catch (error) { } catch (error) {
Swal.fire('Error!', 'Kesalahan koneksi server.', 'error'); Swal.fire('Error!', 'Gagal menghubungi server.', 'error');
} finally { } finally {
btn.disabled = false; btn.disabled = false;
btn.innerText = 'Perbarui Data';
} }
}); });
// Jalankan load data saat halaman siap
document.addEventListener('DOMContentLoaded', loadSiswa);
</script> </script>
@endsection @endsection

View File

@ -75,20 +75,26 @@ class="input-modern w-full text-slate-900"
</div> </div>
<script> <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) { document.getElementById('loginForm').addEventListener('submit', async function(e) {
e.preventDefault(); e.preventDefault();
const btn = document.getElementById('btnLogin'); 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 password = document.getElementById('password').value;
const errorElement = document.getElementById('error-message'); const errorElement = document.getElementById('error-message');
errorElement.classList.add('hidden'); errorElement.classList.add('hidden');
errorElement.innerText = '';
btn.disabled = true; btn.disabled = true;
btn.innerText = 'Memproses...'; btn.innerText = 'Memproses...';
try { try {
const response = await fetch('/api/login', { const response = await fetch(apiLoginUrl, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', '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('access_token', data.access_token);
localStorage.setItem('user_role', data.role); localStorage.setItem('user_role', data.role);
if (data.user) {
localStorage.setItem('user_name', data.user.name || data.user.username || 'User');
}
if (data.role === 'admin') { if (data.role === 'admin') {
window.location.href = "{{ route('admin.dashboard') }}"; window.location.href = adminDashboardUrl;
} else if (data.role === 'siswa') {
window.location.href = studentDashboardUrl;
} else { } else {
window.location.href = "{{ route('student.profile') }}"; errorElement.innerText = 'Role pengguna tidak dikenali.';
errorElement.classList.remove('hidden');
} }
} else { } else {
errorElement.innerText = data.message || 'Username atau password salah.'; 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"> <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"> <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="max-w-7xl mx-auto flex justify-between items-center gap-4">
<div class="flex 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"> <div class="w-10 h-10 bg-emerald-100 rounded-xl flex items-center justify-center text-emerald-600 font-bold">
AS AS
</div> </div>
<span class="font-black text-xl tracking-tighter text-emerald-950 uppercase italic"> <span class="font-black text-xl tracking-tighter text-emerald-950 uppercase italic">
ASGARD. ASGARD.
</span> </span>
@ -129,19 +129,15 @@
Cek Rekomendasi Cek Rekomendasi
</a> </a>
<!-- Menu ini membuka hasil rekomendasi terakhir -->
<a href="#" onclick="goToLastResult(event)" class="hover:text-emerald-600 transition"> <a href="#" onclick="goToLastResult(event)" class="hover:text-emerald-600 transition">
Hasil Rekomendasi Hasil Rekomendasi
</a> </a>
<!-- <a href="#" class="hover:text-emerald-600 transition">
Kontak BK
</a> -->
</div> </div>
<div class="flex items-center gap-3"> <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="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> <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"> <span id="student-name-label" class="text-[10px] font-bold text-emerald-700 uppercase">
Siswa Siswa
</span> </span>
@ -159,7 +155,6 @@
<main class="max-w-7xl mx-auto px-6 flex-grow py-10"> <main class="max-w-7xl mx-auto px-6 flex-grow py-10">
<!-- Header -->
<header class="mb-10"> <header class="mb-10">
<h1 class="text-4xl font-extrabold text-slate-900 tracking-tight"> <h1 class="text-4xl font-extrabold text-slate-900 tracking-tight">
Halo, Siswa SMAPA! 👋 Halo, Siswa SMAPA! 👋
@ -170,7 +165,6 @@
</p> </p>
</header> </header>
<!-- Statistik -->
<section class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-10"> <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"> <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"> <h4 class="text-slate-400 text-xs font-bold uppercase tracking-widest mb-4">
@ -213,7 +207,6 @@
</div> </div>
</section> </section>
<!-- Akses cepat -->
<section class="bg-white p-8 md:p-10 rounded-[2.5rem] card-shadow border border-slate-100 mb-10"> <section class="bg-white p-8 md:p-10 rounded-[2.5rem] card-shadow border border-slate-100 mb-10">
<div class="mb-8"> <div class="mb-8">
<h3 class="text-xl font-black text-slate-900"> <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"> <h4 class="font-black text-blue-900 text-sm">
Cek Rekomendasi Baru Cek Rekomendasi Baru
</h4> </h4>
<p class="text-[11px] text-blue-700 font-semibold mt-1"> <p class="text-[11px] text-blue-700 font-semibold mt-1">
Isi data kembali untuk mendapatkan hasil terbaru. Isi data kembali untuk mendapatkan hasil terbaru.
</p> </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"> <h4 class="font-black text-emerald-900 text-sm">
Hasil Rekomendasi Hasil Rekomendasi
</h4> </h4>
<p class="text-[11px] text-emerald-700 font-semibold mt-1"> <p class="text-[11px] text-emerald-700 font-semibold mt-1">
Buka kembali hasil rekomendasi terakhir. Buka kembali hasil rekomendasi terakhir.
</p> </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"> <h4 class="font-black text-violet-900 text-sm">
Riwayat Rekomendasi Riwayat Rekomendasi
</h4> </h4>
<p class="text-[11px] text-violet-700 font-semibold mt-1"> <p class="text-[11px] text-violet-700 font-semibold mt-1">
Lihat daftar rekomendasi yang pernah dibuat. Lihat daftar rekomendasi yang pernah dibuat.
</p> </p>
@ -276,7 +272,6 @@ class="flex items-center p-6 bg-violet-50 rounded-2xl border border-violet-100 c
</div> </div>
</section> </section>
<!-- Grafik -->
<section class="bg-white p-8 md:p-10 rounded-[2.5rem] card-shadow border border-slate-100 mb-10"> <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"> <h3 class="text-xl font-black text-slate-900 mb-2">
Perkembangan Rata-Rata Nilai 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> </p>
</section> </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"> <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="flex flex-col lg:flex-row gap-8">
<div class="lg:w-1/3"> <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> </div>
</section> </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"> <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"> <div class="mb-6">
<h3 class="text-xl font-black text-slate-900"> <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> </section>
</main> </main>
<!-- Footer -->
<footer class="bg-[#062C22] text-white py-16 px-6"> <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="max-w-7xl mx-auto grid grid-cols-1 md:grid-cols-3 gap-12">
<div class="space-y-6"> <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 Panduan Sistem
</a> </a>
</li> </li>
<!-- <li>
<a href="#" class="hover:text-white transition">
Kebijakan Privasi
</a>
</li> -->
</ul> </ul>
</div> </div>
@ -421,17 +408,57 @@ class="flex items-center p-6 bg-violet-50 rounded-2xl border border-violet-100 c
<script> <script>
let trendChartInstance = null; let trendChartInstance = null;
const routeLogin = @json(route('login')); function getAppBaseUrl() {
const routeDashboard = @json(route('student.profile')); const origin = window.location.origin;
const routePrediction = @json(route('student.prediction')); const segments = window.location.pathname.split('/').filter(Boolean);
const routeAdminDashboard = @json(route('admin.dashboard')); 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() { function getAccessToken() {
return localStorage.getItem('access_token'); return localStorage.getItem('access_token') ||
localStorage.getItem('token') ||
sessionStorage.getItem('access_token') ||
sessionStorage.getItem('token') ||
'';
} }
function getUserRole() { 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) { function redirectByRole(role) {
@ -440,7 +467,7 @@ function redirectByRole(role) {
return; return;
} }
if (role === 'siswa') { if (role === 'siswa' || role === 'student') {
window.location.href = routeDashboard; window.location.href = routeDashboard;
return; return;
} }
@ -448,6 +475,37 @@ function redirectByRole(role) {
window.location.href = routeLogin; 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() { function getStoredLastResult() {
try { try {
const saved = localStorage.getItem('asgard_last_result'); const saved = localStorage.getItem('asgard_last_result');
@ -466,7 +524,7 @@ function saveStoredLastResult(data) {
id: data.id || oldData.id || '', id: data.id || oldData.id || '',
jurusan: data.jurusan || oldData.jurusan || '', jurusan: data.jurusan || oldData.jurusan || '',
tanggal: data.tanggal || oldData.tanggal || '', tanggal: data.tanggal || oldData.tanggal || '',
url: data.url || oldData.url || '' url: normalizeAppUrl(data.url || oldData.url || '')
}; };
localStorage.setItem('asgard_last_result', JSON.stringify(nextData)); localStorage.setItem('asgard_last_result', JSON.stringify(nextData));
@ -478,12 +536,12 @@ function goToLastResult(event) {
const saved = getStoredLastResult(); const saved = getStoredLastResult();
if (saved && saved.url) { if (saved && saved.url) {
window.location.href = saved.url; window.location.href = normalizeAppUrl(saved.url);
return; return;
} }
if (saved && saved.id) { if (saved && saved.id) {
window.location.href = `/student/result/${saved.id}`; window.location.href = makeResultUrl(saved.id);
return; return;
} }
@ -504,7 +562,7 @@ function getHistoryId(item) {
} }
function makeResultUrl(id) { function makeResultUrl(id) {
return id ? `/student/result/${id}` : ''; return id ? studentResultUrlTemplate.replace('__ID__', encodeURIComponent(id)) : '';
} }
function isValidRecommendationName(value) { function isValidRecommendationName(value) {
@ -524,7 +582,7 @@ function isValidRecommendationName(value) {
if (token) { if (token) {
try { try {
await fetch('/api/logout', { await fetch(apiLogoutUrl, {
method: 'POST', method: 'POST',
headers: { headers: {
'Authorization': `Bearer ${token}`, 'Authorization': `Bearer ${token}`,
@ -536,8 +594,7 @@ function isValidRecommendationName(value) {
} }
} }
localStorage.removeItem('access_token'); clearLoginStorage();
localStorage.removeItem('user_role');
localStorage.removeItem('asgard_last_result'); localStorage.removeItem('asgard_last_result');
window.location.href = routeLogin; window.location.href = routeLogin;
} }
@ -691,7 +748,7 @@ function renderTrendChart(tren) {
return; return;
} }
if (role !== 'siswa') { if (role !== 'siswa' && role !== 'student') {
redirectByRole(role); redirectByRole(role);
return; return;
} }
@ -700,28 +757,24 @@ function renderTrendChart(tren) {
document.getElementById('student-name-label').innerText = savedName; document.getElementById('student-name-label').innerText = savedName;
try { try {
const response = await fetch('/api/student/dashboard-stats', { const response = await fetch(apiStudentDashboardStatsUrl, {
method: 'GET',
headers: { headers: {
'Authorization': `Bearer ${token}`, 'Authorization': `Bearer ${token}`,
'Accept': 'application/json' 'Accept': 'application/json'
} }
}); });
if (response.status === 401) { const result = await parseJsonResponse(response);
localStorage.removeItem('access_token');
localStorage.removeItem('user_role'); if (response.status === 401 || response.status === 403) {
clearLoginStorage();
window.location.href = routeLogin; window.location.href = routeLogin;
return; return;
} }
const result = await response.json(); if (!response.ok || result.success === false) {
throw new Error(result.message || 'Gagal memuat data dashboard.');
if (!result.success) {
document.getElementById('stat-total').innerText = 0;
document.getElementById('stat-last').innerText = 'Belum ada rekomendasi';
renderActivityList([]);
renderTrendChart([]);
return;
} }
const data = result.data || {}; const data = result.data || {};
@ -736,7 +789,6 @@ function renderTrendChart(tren) {
? lastName ? lastName
: 'Belum ada rekomendasi'; : 'Belum ada rekomendasi';
// Simpan hasil terakhir dari data dashboard jika API mengirim id riwayat
const firstId = getHistoryId(firstActivity); const firstId = getHistoryId(firstActivity);
if (isValidRecommendationName(lastName) && firstId) { if (isValidRecommendationName(lastName) && firstId) {

View File

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

View File

@ -6,98 +6,34 @@
<title>Cek Rekomendasi - ASGARD</title> <title>Cek Rekomendasi - ASGARD</title>
<script src="https://cdn.tailwindcss.com"></script> <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() }}"> <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> <style>
body { body {
font-family: 'Plus Jakarta Sans', sans-serif; font-family: 'Plus Jakarta Sans', sans-serif;
background: background: linear-gradient(180deg, #f8fafc 0%, #f3f7fb 50%, #f8fafc 100%);
radial-gradient(circle at 8% 12%, rgba(16, 185, 129, 0.10), transparent 32%),
radial-gradient(circle at 92% 8%, rgba(59, 130, 246, 0.08), transparent 30%),
linear-gradient(180deg, #f8fafc 0%, #f3f7fb 50%, #f8fafc 100%);
} }
nav { nav {
box-shadow: 0 10px 35px rgba(15, 23, 42, 0.05); 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 { .card-shadow {
box-shadow: 0 18px 50px rgba(15, 23, 42, 0.08); box-shadow: 0 18px 50px rgba(15, 23, 42, 0.08);
} }
main > div > div.bg-white {
background: rgba(255, 255, 255, 0.94);
backdrop-filter: blur(16px);
box-shadow: 0 24px 70px rgba(15, 23, 42, 0.08);
}
main section {
transition: transform 0.22s ease, box-shadow 0.22s ease, border-color 0.22s ease;
}
main section:hover {
transform: translateY(-2px);
box-shadow: 0 16px 35px rgba(15, 23, 42, 0.055);
border-color: rgba(16, 185, 129, 0.18);
}
input,
select {
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease, background-color 0.2s ease;
}
input:hover,
select:hover {
border-color: rgba(16, 185, 129, 0.45);
background-color: #ffffff;
}
input:focus,
select:focus {
transform: translateY(-1px);
}
#alert-box {
box-shadow: 0 14px 35px rgba(239, 68, 68, 0.08);
}
footer {
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
}
</style> </style>
</head> </head>
<body class="flex flex-col min-h-screen text-slate-800"> <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"> <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="max-w-7xl mx-auto flex items-center justify-between gap-4">
<div class="flex 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"> <div class="w-10 h-10 bg-emerald-100 rounded-xl flex items-center justify-center text-emerald-600 font-bold">
AS AS
</div> </div>
<span class="font-black text-xl tracking-tighter text-emerald-950 uppercase italic"> <span class="font-black text-xl tracking-tighter text-emerald-950 uppercase italic">
ASGARD. ASGARD.
</span> </span>
@ -112,30 +48,16 @@
Cek Rekomendasi Cek Rekomendasi
</a> </a>
<!-- Menu untuk membuka hasil terakhir -->
<a href="#" onclick="goToLastResult(event)" class="hover:text-emerald-600 transition"> <a href="#" onclick="goToLastResult(event)" class="hover:text-emerald-600 transition">
Hasil Rekomendasi Hasil Rekomendasi
</a> </a>
<!-- <a href="#" class="hover:text-emerald-600 transition">
Kontak BK
</a> -->
</div> </div>
<div class="flex items-center gap-3"> <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">
<div class="hidden sm:flex items-center gap-3 bg-emerald-50 px-4 py-2 rounded-full border border-emerald-100"> <span class="text-[10px] font-bold uppercase tracking-widest">
<div class="w-2 h-2 bg-emerald-500 rounded-full animate-pulse"></div> Logout
<span class="text-[10px] font-bold text-emerald-700 uppercase"> </span>
Siswa </button>
</span>
</div>
<button onclick="handleLogout()" class="bg-red-50 hover:bg-red-100 text-red-600 border border-red-100 px-4 py-2 rounded-full transition-all">
<span class="text-[10px] font-bold uppercase tracking-widest">
Logout
</span>
</button>
</div>
</div> </div>
</nav> </nav>
@ -153,7 +75,7 @@
</h1> </h1>
<p class="text-slate-500 mt-3 text-sm md:text-base leading-relaxed"> <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> </p>
</div> </div>
@ -161,7 +83,6 @@
<div class="space-y-6"> <div class="space-y-6">
<!-- Kondisi ekonomi -->
<section class="bg-slate-50 border border-slate-100 rounded-3xl p-5 md:p-6"> <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"> <h2 class="font-extrabold text-slate-800 mb-1">
💰 Kondisi Ekonomi 💰 Kondisi Ekonomi
@ -176,15 +97,9 @@
id="gaji_ortu" id="gaji_ortu"
min="0" min="0"
placeholder="Contoh: 3000000" 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" 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>
</section> </section>
<!-- Minat dan bakat -->
<section class="bg-emerald-50/40 border border-emerald-100 rounded-3xl p-5 md:p-6"> <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"> <h2 class="font-extrabold text-emerald-800 mb-4">
🎯 Kuesioner Minat dan Bakat 🎯 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> </div>
</section> </section>
<!-- Nilai rapor -->
<section class="bg-slate-50 border border-slate-100 rounded-3xl p-5 md:p-6"> <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"> <h2 class="font-extrabold text-slate-700 mb-2">
📚 Nilai Rapor 📚 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> </p>
<div class="grid grid-cols-2 md:grid-cols-5 gap-4"> <div class="grid grid-cols-2 md:grid-cols-5 gap-4">
<div> @php
<label class="block text-xs font-bold text-slate-500 mb-2">Agama</label> $mapel = [
<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"> 'agama' => 'Agama',
</div> '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> @foreach ($mapel as $id => $label)
<label class="block text-xs font-bold text-slate-500 mb-2">PKN</label> <div>
<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"> <label class="block text-xs font-bold text-slate-500 mb-2">
</div> {{ $label }}
</label>
<div> <input
<label class="block text-xs font-bold text-slate-500 mb-2">B. Indonesia</label> type="number"
<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"> id="{{ $id }}"
</div> min="0"
max="100"
<div> placeholder="0 - 100"
<label class="block text-xs font-bold text-slate-500 mb-2">B. Inggris</label> 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">
<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> @endforeach
<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>
</div> </div>
</section> </section>
<!-- Tombol --> <button
<div class="flex flex-col sm:flex-row gap-3 pt-2"> type="button"
<a href="{{ route('student.profile') }}" id="btnSubmit"
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"> class="w-full bg-emerald-600 hover:bg-emerald-700 text-white font-extrabold py-4 px-6 rounded-2xl transition">
Kembali Proses Rekomendasi
</a> </button>
<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>
</div> </div>
</div> </div>
</div> </div>
</main> </main>
<!-- Footer --> <footer class="bg-[#062C22] text-white py-10 px-6 mt-10">
<footer class="bg-[#062C22] text-white py-16 px-6 mt-16"> <div class="max-w-7xl mx-auto text-center">
<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">
<p class="text-[10px] font-medium uppercase tracking-[0.35em] text-slate-400"> <p class="text-[10px] font-medium uppercase tracking-[0.35em] text-slate-400">
© 2026 GALANG SEFIAN ADJI - POLITEKNIK NEGERI JEMBER © 2026 GALANG SEFIAN ADJI - POLITEKNIK NEGERI JEMBER
</p> </p>
@ -431,39 +256,60 @@ class="w-full sm:w-2/3 bg-emerald-600 hover:bg-emerald-700 text-white font-bold
</footer> </footer>
<script> <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 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() { function getAccessToken() {
return localStorage.getItem('access_token'); return localStorage.getItem('access_token') ||
localStorage.getItem('token') ||
sessionStorage.getItem('access_token') ||
sessionStorage.getItem('token') ||
'';
} }
function getUserRole() { 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) { function redirectByRole(role) {
@ -472,7 +318,7 @@ function redirectByRole(role) {
return; return;
} }
if (role === 'siswa') { if (role === 'siswa' || role === 'student') {
window.location.href = routeDashboard; window.location.href = routeDashboard;
return; return;
} }
@ -480,6 +326,41 @@ function redirectByRole(role) {
window.location.href = routeLogin; 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() { function getStoredLastResult() {
try { try {
const saved = localStorage.getItem('asgard_last_result'); const saved = localStorage.getItem('asgard_last_result');
@ -495,12 +376,12 @@ function goToLastResult(event) {
const saved = getStoredLastResult(); const saved = getStoredLastResult();
if (saved && saved.url) { if (saved && saved.url) {
window.location.href = saved.url; window.location.href = normalizeAppUrl(saved.url);
return; return;
} }
if (saved && saved.id) { if (saved && saved.id) {
window.location.href = `/student/result/${saved.id}`; window.location.href = makeResultUrl(saved.id);
return; return;
} }
@ -508,44 +389,41 @@ function goToLastResult(event) {
window.location.href = routePrediction; window.location.href = routePrediction;
} }
function validateForm(payload) { async function handleLogout() {
const scoreFields = [ const token = getAccessToken();
'agama', 'pkn', 'bahasa_indonesia', 'bahasa_inggris', 'matematika',
'fisika', 'kimia', 'biologi', 'sejarah', 'geografi',
'ekonomi_mapel', 'sosiologi', 'seni_budaya', 'pjok', 'informatika'
];
for (const field of scoreFields) { if (token) {
const rawValue = document.getElementById(field).value; try {
await fetch(apiLogoutUrl, {
if (rawValue === '') { method: 'POST',
return `Nilai ${field.replaceAll('_', ' ')} wajib diisi.`; headers: {
} 'Authorization': `Bearer ${token}`,
'Accept': 'application/json'
const value = Number(payload[field]); }
});
if (isNaN(value) || value < 0 || value > 100) { } catch (error) {
return `Nilai ${field.replaceAll('_', ' ')} harus berada pada rentang 0 sampai 100.`; console.error('Logout error:', error);
} }
} }
if (document.getElementById('gaji_ortu').value === '') { clearLoginStorage();
return 'Pendapatan gabungan orang tua wajib diisi.'; localStorage.removeItem('asgard_last_result');
} window.location.href = routeLogin;
}
if (Number(payload.gaji_ortu) < 0) { function showError(message) {
return 'Pendapatan gabungan orang tua tidak boleh bernilai negatif.'; 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) { function getNumberValue(id) {
if (!document.getElementById(id).value) { const value = document.getElementById(id).value;
return 'Semua pertanyaan minat dan bakat wajib diisi.'; return value === '' ? null : Number(value);
}
}
return null;
} }
function calculateRiasecScores() { function calculateRiasecScores() {
@ -558,12 +436,10 @@ function calculateRiasecScores() {
c: 0 c: 0
}; };
const questionIds = ['q1', 'q2', 'q3', 'q4', 'q5']; ['q1', 'q2', 'q3', 'q4', 'q5'].forEach(id => {
questionIds.forEach(id => {
const value = document.getElementById(id).value; const value = document.getElementById(id).value;
if (value && scores.hasOwnProperty(value)) { if (value && Object.prototype.hasOwnProperty.call(scores, value)) {
scores[value] += 20; scores[value] += 20;
} }
}); });
@ -571,27 +447,54 @@ function calculateRiasecScores() {
return scores; return scores;
} }
async function handleLogout() { function validateForm(payload) {
const token = getAccessToken(); const scoreFields = [
'agama',
'pkn',
'bahasa_indonesia',
'bahasa_inggris',
'matematika',
'fisika',
'kimia',
'biologi',
'sejarah',
'geografi',
'ekonomi_mapel',
'sosiologi',
'seni_budaya',
'pjok',
'informatika'
];
if (token) { for (const field of scoreFields) {
try { const rawValue = document.getElementById(field).value;
await fetch('/api/logout', {
method: 'POST', if (rawValue === '') {
headers: { return `Nilai ${field.replaceAll('_', ' ')} wajib diisi.`;
'Authorization': `Bearer ${token}`, }
'Accept': 'application/json'
} const value = Number(payload[field]);
});
} catch (error) { if (Number.isNaN(value) || value < 0 || value > 100) {
console.error('Logout error:', error); return `Nilai ${field.replaceAll('_', ' ')} harus berada pada rentang 0 sampai 100.`;
} }
} }
localStorage.removeItem('access_token'); if (document.getElementById('gaji_ortu').value === '') {
localStorage.removeItem('user_role'); return 'Pendapatan gabungan orang tua wajib diisi.';
localStorage.removeItem('asgard_last_result'); }
window.location.href = routeLogin;
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 () { document.addEventListener('DOMContentLoaded', function () {
@ -603,9 +506,8 @@ function calculateRiasecScores() {
return; return;
} }
if (role !== 'siswa') { if (role !== 'siswa' && role !== 'student') {
redirectByRole(role); redirectByRole(role);
return;
} }
}); });
@ -625,12 +527,12 @@ function calculateRiasecScores() {
return; return;
} }
if (role !== 'siswa') { if (role !== 'siswa' && role !== 'student') {
redirectByRole(role); redirectByRole(role);
return; return;
} }
btnSubmit.innerHTML = 'Memproses Rekomendasi...'; btnSubmit.innerText = 'Memproses Rekomendasi...';
btnSubmit.disabled = true; btnSubmit.disabled = true;
const scores = calculateRiasecScores(); const scores = calculateRiasecScores();
@ -651,12 +553,14 @@ function calculateRiasecScores() {
seni_budaya: getNumberValue('seni_budaya'), seni_budaya: getNumberValue('seni_budaya'),
pjok: getNumberValue('pjok'), pjok: getNumberValue('pjok'),
informatika: getNumberValue('informatika'), informatika: getNumberValue('informatika'),
riasec_r: scores.r, riasec_r: scores.r,
riasec_i: scores.i, riasec_i: scores.i,
riasec_a: scores.a, riasec_a: scores.a,
riasec_s: scores.s, riasec_s: scores.s,
riasec_e: scores.e, riasec_e: scores.e,
riasec_c: scores.c, riasec_c: scores.c,
gaji_ortu: getNumberValue('gaji_ortu') gaji_ortu: getNumberValue('gaji_ortu')
}; };
@ -664,34 +568,27 @@ function calculateRiasecScores() {
if (validationError) { if (validationError) {
showError(validationError); showError(validationError);
btnSubmit.innerHTML = 'Proses Rekomendasi'; btnSubmit.innerText = 'Proses Rekomendasi';
btnSubmit.disabled = false; btnSubmit.disabled = false;
return; return;
} }
try { try {
const response = await fetch('/api/student/proses-rekomendasi', { const response = await fetch(apiProsesRekomendasiUrl, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json', 'Accept': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'Authorization': `Bearer ${token}` 'Authorization': `Bearer ${token}`
}, },
body: JSON.stringify(payload) body: JSON.stringify(payload)
}); });
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 = result.redirect_url; clearLoginStorage();
return;
}
if (response.status === 401) {
showError('Sesi login berakhir. Silakan login kembali.'); showError('Sesi login berakhir. Silakan login kembali.');
localStorage.removeItem('access_token');
localStorage.removeItem('user_role');
setTimeout(() => { setTimeout(() => {
window.location.href = routeLogin; window.location.href = routeLogin;
@ -700,17 +597,31 @@ function calculateRiasecScores() {
return; return;
} }
if (response.status === 403) { if (response.ok && result.success) {
showError(result.message || 'Anda tidak memiliki akses ke halaman ini.'); 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; return;
} }
showError(result.message || 'Terjadi kesalahan saat memproses rekomendasi.'); showError(result.message || 'Terjadi kesalahan saat memproses rekomendasi.');
} catch (error) { } 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 { } finally {
btnSubmit.innerHTML = 'Proses Rekomendasi'; btnSubmit.innerText = 'Proses Rekomendasi';
btnSubmit.disabled = false; btnSubmit.disabled = false;
} }
}); });

View File

@ -12,25 +12,75 @@
use App\Http\Controllers\Api\Student\DashboardStudentController; use App\Http\Controllers\Api\Student\DashboardStudentController;
use Illuminate\Support\Facades\Route; 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('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); | Admin Routes Group
Route::apiResource('alumni', AlumniController::class); |--------------------------------------------------------------------------
Route::apiResource('prospek-kerja', ProspekKerjaController::class); */
Route::get('/dashboard-stats', [DashboardAdminController::class, 'index']); Route::middleware('role:admin')->prefix('admin')->name('api.admin.')->group(function () {
Route::get('/riwayat', [RiwayatController::class, 'index']); // Dashboard Stats
Route::get('/riwayat/{id}', [RiwayatController::class, 'show']); 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']); | Student (Siswa) Routes Group
Route::get('/hasil-rekomendasi/{id}', [RecommendationApiController::class, 'getHasilRekomendasi']); |--------------------------------------------------------------------------
*/
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::prefix('admin')->name('admin.')->group(function () {
Route::get('/dashboard', [AdminDashboardController::class, 'index'])->name('dashboard'); 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('siswa', SiswaController::class)->only(['index', 'create', 'edit'])->names(['index' => 'siswa']);
Route::resource('alumni', AlumniController::class)->only(['index', 'create', 'edit'])->names(['index' => 'alumni']); 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('jurusan', JurusanController::class)->only(['index', 'create', 'edit'])->names(['index' => 'jurusan']);
Route::resource('guru', GuruController::class)->only(['index', 'create', 'edit'])->names(['index' => 'guru']); 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::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 () { Route::get('/riwayat', [RiwayatController::class, 'index'])->name('riwayat');
return view('admin.latihmodel'); Route::get('/latih-model', function() {
// Logika latih model Anda
})->name('latih-model'); })->name('latih-model');
}); });