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

@ -6,13 +6,15 @@
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)
{ {
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', ''));
@ -31,6 +33,7 @@ public function index(Request $request)
return response()->json([ return response()->json([
'success' => true, 'success' => true,
'message' => 'Data alumni berhasil dimuat.',
'data' => $alumni->items(), 'data' => $alumni->items(),
'current_page' => $alumni->currentPage(), 'current_page' => $alumni->currentPage(),
'last_page' => $alumni->lastPage(), 'last_page' => $alumni->lastPage(),
@ -39,69 +42,159 @@ public function index(Request $request)
'from' => $alumni->firstItem(), 'from' => $alumni->firstItem(),
'to' => $alumni->lastItem(), 'to' => $alumni->lastItem(),
], 200); ], 200);
} catch (Throwable $e) {
return response()->json([
'success' => false,
'message' => 'Gagal memuat data alumni.',
'error' => $e->getMessage(),
], 500);
}
} }
public function store(Request $request) public function store(Request $request)
{ {
try {
$validator = Validator::make($request->all(), [ $validator = Validator::make($request->all(), [
'nama_alumni' => 'required|string|max:255', 'nama_alumni' => 'required|string|max:255',
'perguruan_tinggi' => 'required|string', 'perguruan_tinggi' => 'required|string|max:255',
'jurusan_diambil' => 'required|string', 'jurusan_diambil' => 'required|string|max:255',
], [ ], [
'nama_alumni.required' => 'Nama alumni wajib diisi.', 'nama_alumni.required' => 'Nama alumni wajib diisi.',
'perguruan_tinggi.required' => 'Nama Perguruan Tinggi harus diisi.', 'perguruan_tinggi.required' => 'Nama perguruan tinggi wajib diisi.',
'jurusan_diambil.required' => 'Jurusan yang diambil tidak boleh kosong.', '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 = Alumni::create($request->all()); $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([ return response()->json([
'success' => true, 'success' => true,
'message' => 'Alumni ' . $alumni->id_alumni . ' berhasil ditambahkan.', 'message' => 'Data alumni berhasil ditambahkan.',
'data' => $alumni 'data' => $alumni,
], 201); ], 201);
} catch (Throwable $e) {
return response()->json([
'success' => false,
'message' => 'Terjadi kesalahan saat menyimpan data alumni.',
'error' => $e->getMessage(),
], 500);
}
}
public function show($id)
{
try {
$alumni = Alumni::find($id);
if (!$alumni) {
return response()->json([
'success' => false,
'message' => 'Data alumni tidak ditemukan.',
], 404);
}
return response()->json([
'success' => true,
'message' => 'Data alumni berhasil ditemukan.',
'data' => $alumni,
], 200);
} catch (Throwable $e) {
return response()->json([
'success' => false,
'message' => 'Terjadi kesalahan saat mengambil data alumni.',
'error' => $e->getMessage(),
], 500);
}
} }
public function update(Request $request, $id) public function update(Request $request, $id)
{ {
try {
$alumni = Alumni::find($id); $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(), [ $validator = Validator::make($request->all(), [
'nama_alumni' => 'required|string|max:255', 'nama_alumni' => 'required|string|max:255',
'perguruan_tinggi' => 'required|string', 'perguruan_tinggi' => 'required|string|max:255',
'jurusan_diambil' => 'required|string', '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()) { 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->update($request->all()); $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([ return response()->json([
'success' => true, 'success' => true,
'message' => 'Data ' . $alumni->id_alumni . ' berhasil diperbarui' 'message' => 'Data alumni berhasil diperbarui.',
'data' => $alumni,
], 200); ], 200);
} catch (Throwable $e) {
return response()->json([
'success' => false,
'message' => 'Terjadi kesalahan saat memperbarui data alumni.',
'error' => $e->getMessage(),
], 500);
}
} }
public function destroy($id) public function destroy($id)
{ {
try {
$alumni = Alumni::find($id); $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(); $alumni->delete();
return response()->json(['success' => true, 'message' => 'Data Alumni berhasil dihapus'], 200); 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);
}
} }
} }

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,6 +46,9 @@ 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.';
$distribusiJurusan = collect();
if (Schema::hasTable($tableName) && Schema::hasColumn($tableName, 'hasil_rekomendasi_jurusan')) {
$selectDistribusi = 'hasil_rekomendasi_jurusan as jurusan, COUNT(*) as total'; $selectDistribusi = 'hasil_rekomendasi_jurusan as jurusan, COUNT(*) as total';
if ($kolomAkurasi) { if ($kolomAkurasi) {
@ -66,23 +70,40 @@ public function index(Request $request)
: null, : 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,19 +4,27 @@
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)
{ {
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]) ? $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) {
@ -29,73 +37,209 @@ public function index(Request $request)
return response()->json([ return response()->json([
'success' => true, 'success' => true,
'message' => 'Data guru berhasil dimuat.',
'data' => $guru->items(), 'data' => $guru->items(),
'current_page' => $guru->currentPage(), 'current_page' => $guru->currentPage(),
'last_page' => $guru->lastPage(), 'last_page' => $guru->lastPage(),
'per_page' => $guru->perPage(), 'per_page' => $guru->perPage(),
'total' => $guru->total(), 'total' => $guru->total(),
'from' => $guru->firstItem(),
'to' => $guru->lastItem(),
], 200); ], 200);
} catch (Throwable $e) {
return response()->json([
'success' => false,
'message' => 'Terjadi kesalahan saat memuat data guru.',
'error' => $e->getMessage()
], 500);
}
} }
/**
* 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();
try {
// Create User Akun
$user = User::create([
'name' => $request->nama_guru,
'username' => $request->username,
'email' => $request->email,
'password' => Hash::make($request->password),
'role' => 'admin',
]);
// Create Data Guru terikat dengan user_id
$guru = Guru::create([
'user_id' => $user->id,
'nama_guru' => $request->nama_guru,
]);
DB::commit();
$guru->load('user');
return response()->json([ return response()->json([
'success' => true, 'success' => true,
'message' => 'Guru ' . $guru->id_guru . ' berhasil ditambahkan.', 'message' => 'Data guru dan akun login berhasil disimpan.',
'data' => $guru 'data' => $guru
], 201); ], 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)
{ {
try {
$guru = Guru::find($id); $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(), [ $validator = Validator::make($request->all(), [
'nama_guru' => 'required|string|max:255', 'nama_guru' => 'required|string|max:255',
], [
'nama_guru.required' => 'Update gagal, nama guru harus 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);
} }
$guru->update($request->all()); 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([ return response()->json([
'success' => true, 'success' => true,
'message' => 'Data ' . $guru->id_guru . ' berhasil diperbarui' 'message' => 'Data guru berhasil diperbarui.',
'data' => $guru
], 200); ], 200);
} catch (Throwable $e) {
DB::rollBack();
return response()->json([
'success' => false,
'message' => 'Terjadi kesalahan saat memperbarui data guru.',
'error' => $e->getMessage()
], 500);
}
} }
/**
* Remove the specified resource from storage.
*/
public function destroy($id) public function destroy($id)
{ {
try {
$guru = Guru::find($id); $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(); $guru->delete();
return response()->json(['success' => true, 'message' => 'Data Guru berhasil dihapus'], 200); 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);
}
} }
} }

View File

@ -6,23 +6,30 @@
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)
{ {
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', ''));
$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}%");
});
}); });
} }
@ -30,6 +37,7 @@ public function index(Request $request)
return response()->json([ return response()->json([
'success' => true, 'success' => true,
'message' => 'Data jurusan berhasil dimuat.',
'data' => $jurusan->items(), 'data' => $jurusan->items(),
'current_page' => $jurusan->currentPage(), 'current_page' => $jurusan->currentPage(),
'last_page' => $jurusan->lastPage(), 'last_page' => $jurusan->lastPage(),
@ -38,66 +46,151 @@ public function index(Request $request)
'from' => $jurusan->firstItem(), 'from' => $jurusan->firstItem(),
'to' => $jurusan->lastItem(), 'to' => $jurusan->lastItem(),
], 200); ], 200);
} catch (Throwable $e) {
return response()->json([
'success' => false,
'message' => 'Terjadi kesalahan saat memuat data jurusan.',
'error' => $e->getMessage(),
], 500);
}
} }
public function store(Request $request) public function store(Request $request)
{ {
try {
$validator = Validator::make($request->all(), [ $validator = Validator::make($request->all(), [
'nama_jurusan' => 'required|string|max:255', 'nama_jurusan' => 'required|string|max:255',
'rumpun' => 'required|string', '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 = Jurusan::create($request->all()); $jurusan = new Jurusan();
$jurusan->nama_jurusan = $request->input('nama_jurusan');
$jurusan->rumpun = $request->input('rumpun');
$jurusan->save();
return response()->json([ return response()->json([
'success' => true, 'success' => true,
'message' => 'Jurusan berhasil ditambahkan', 'message' => 'Data jurusan berhasil ditambahkan.',
'data' => $jurusan 'data' => $jurusan,
], 201); ], 201);
} catch (Throwable $e) {
return response()->json([
'success' => false,
'message' => 'Terjadi kesalahan saat menyimpan data jurusan.',
'error' => $e->getMessage(),
], 500);
}
} }
public function show($id) public function show($id)
{ {
try {
$jurusan = Jurusan::find($id); $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, 'data' => $jurusan], 200); 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);
}
} }
public function update(Request $request, $id) public function update(Request $request, $id)
{ {
try {
$jurusan = Jurusan::find($id); $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->update($request->all()); $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([ return response()->json([
'success' => true, 'success' => true,
'message' => 'Jurusan berhasil diperbarui', 'message' => 'Data jurusan berhasil diperbarui.',
'data' => $jurusan 'data' => $jurusan,
], 200); ], 200);
} catch (Throwable $e) {
return response()->json([
'success' => false,
'message' => 'Terjadi kesalahan saat memperbarui data jurusan.',
'error' => $e->getMessage(),
], 500);
}
} }
public function destroy($id) public function destroy($id)
{ {
try {
$jurusan = Jurusan::find($id); $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(); $jurusan->delete();
return response()->json(['success' => true, 'message' => 'Jurusan berhasil dihapus'], 200); 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);
}
} }
} }

View File

@ -6,13 +6,15 @@
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)
{ {
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', ''));
@ -33,6 +35,7 @@ public function index(Request $request)
return response()->json([ return response()->json([
'success' => true, 'success' => true,
'message' => 'Data prospek kerja berhasil dimuat.',
'data' => $prospek->items(), 'data' => $prospek->items(),
'current_page' => $prospek->currentPage(), 'current_page' => $prospek->currentPage(),
'last_page' => $prospek->lastPage(), 'last_page' => $prospek->lastPage(),
@ -41,79 +44,161 @@ public function index(Request $request)
'from' => $prospek->firstItem(), 'from' => $prospek->firstItem(),
'to' => $prospek->lastItem(), 'to' => $prospek->lastItem(),
], 200); ], 200);
} catch (Throwable $e) {
return response()->json([
'success' => false,
'message' => 'Gagal memuat data prospek kerja.',
'error' => $e->getMessage(),
], 500);
}
} }
public function store(Request $request) public function store(Request $request)
{ {
try {
$validator = Validator::make($request->all(), [ $validator = Validator::make($request->all(), [
'jurusan_id' => 'required|exists:jurusans,id', 'jurusan_id' => 'required|integer|exists:jurusans,id',
'nama_profesi' => 'required|string|max:255', 'nama_profesi' => 'required|string|max:255',
'range_gaji' => 'nullable|string|max:255', 'range_gaji' => 'nullable|string|max:255',
'deskripsi_pekerjaan' => 'nullable|string', '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 = ProspekKerja::create($request->all()); $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([ return response()->json([
'success' => true, 'success' => true,
'message' => 'Prospek Kerja berhasil ditambahkan', 'message' => 'Data prospek kerja berhasil ditambahkan.',
'data' => $prospek 'data' => $prospek,
], 201); ], 201);
} catch (Throwable $e) {
return response()->json([
'success' => false,
'message' => 'Terjadi kesalahan saat menyimpan data prospek kerja.',
'error' => $e->getMessage(),
], 500);
}
} }
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, 'data' => $prospek], 200); 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);
}
} }
public function update(Request $request, $id) public function update(Request $request, $id)
{ {
try {
$prospek = ProspekKerja::find($id); $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(), [ $validator = Validator::make($request->all(), [
'jurusan_id' => 'required|exists:jurusans,id', 'jurusan_id' => 'required|integer|exists:jurusans,id',
'nama_profesi' => 'required|string|max:255', 'nama_profesi' => 'required|string|max:255',
'range_gaji' => 'nullable|string|max:255', 'range_gaji' => 'nullable|string|max:255',
'deskripsi_pekerjaan' => 'nullable|string', '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->update($request->all()); $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([ return response()->json([
'success' => true, 'success' => true,
'message' => 'Prospek Kerja berhasil diperbarui', 'message' => 'Data prospek kerja berhasil diperbarui.',
'data' => $prospek 'data' => $prospek,
], 200); ], 200);
} catch (Throwable $e) {
return response()->json([
'success' => false,
'message' => 'Terjadi kesalahan saat memperbarui data prospek kerja.',
'error' => $e->getMessage(),
], 500);
}
} }
public function destroy($id) public function destroy($id)
{ {
try {
$prospek = ProspekKerja::find($id); $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(); $prospek->delete();
return response()->json(['success' => true, 'message' => 'Prospek Kerja berhasil dihapus'], 200); 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);
}
} }
} }

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,32 +118,57 @@ 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.',
'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); ], 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);
}
} }
private function getAkurasiPrediksi($riwayat): ?float 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
{ {
$nilai = [ $nilai = [
@ -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)
{ {
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', ''));
$query = Siswa::with(['user', 'jurusan'])->orderBy('id', 'desc'); $query = Siswa::with('user')->orderBy('id', 'desc');
if ($search !== '') { if ($search !== '') {
$query->where(function ($q) use ($search) { $query->where(function ($q) use ($search) {
$q->where('id_siswa', 'like', "%{$search}%") $q->where('nama_lengkap', 'like', "%{$search}%")
->orWhere('nama_lengkap', 'like', "%{$search}%")
->orWhere('nisn', 'like', "%{$search}%") ->orWhere('nisn', 'like', "%{$search}%")
->orWhere('kelas', 'like', "%{$search}%") ->orWhere('kelas', 'like', "%{$search}%")
->orWhere('peminatan', 'like', "%{$search}%")
->orWhereHas('user', function ($user) use ($search) { ->orWhereHas('user', function ($user) use ($search) {
$user->where('name', 'like', "%{$search}%") $user->where('username', 'like', "%{$search}%");
->orWhere('email', 'like', "%{$search}%");
})
->orWhereHas('jurusan', function ($jurusan) use ($search) {
$jurusan->where('nama_jurusan', 'like', "%{$search}%");
}); });
}); });
} }
$siswa = $query->paginate($perPage); $siswa = $query->paginate($perPage);
return response()->json([ return response()->json(['success' => true, 'data' => $siswa], 200);
'success' => true, } catch (Throwable $e) {
'data' => $siswa->items(), return response()->json(['success' => false, 'message' => $e->getMessage()], 500);
'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); return response()->json(['success' => true, 'data' => $siswa], 200);
} catch (Throwable $e) {
return response()->json(['success' => false, 'message' => 'Gagal memuat rincian.', 'error' => $e->getMessage()], 500);
}
} }
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;
if ($request->filled('username')) {
$user->username = $request->username;
$usernameClean = str_replace(' ', '', $request->username);
$user->email = strtolower($usernameClean) . '@gmail.com';
}
if ($request->filled('password')) {
$user->password = Hash::make($request->password);
}
$user->save();
}
}
$siswa->update([
'nama_lengkap' => $request->nama_lengkap,
'nisn' => $request->nisn,
'kelas' => $request->kelas
]);
DB::commit();
return response()->json([ return response()->json([
'success' => true, 'success' => true,
'message' => 'Data ' . $siswa->id_siswa . ' berhasil diperbarui' 'message' => 'Data siswa berhasil diperbarui.',
'data' => $siswa->load('user')
], 200); ], 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)
{ {
try {
$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 tidak ditemukan.'], 404);
} }
if ($siswa->user_id) {
$userId = $siswa->user_id;
$siswa->delete(); $siswa->delete();
User::destroy($userId);
} else {
$siswa->delete();
}
return response()->json(['success' => true, 'message' => 'Data berhasil dihapus'], 200); 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);
}
} }
} }

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')) { // 1. Cari data Jurusan di database berdasarkan nama jurusan hasil prediksi
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.'; $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.';
} }
if (str_contains($jurusan, 'kesehatan') || str_contains($jurusan, 'keperawatan') || str_contains($jurusan, 'farmasi')) { // 3. Ambil semua data prospek kerja terkait dan susun menjadi format HTML list yang rapi
return 'Lulusan bidang ini dapat berkarier di rumah sakit, klinik, laboratorium, industri kesehatan, layanan masyarakat, maupun lembaga penelitian kesehatan.'; $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>';
} }
if (str_contains($jurusan, 'pendidikan') || str_contains($jurusan, 'guru')) { $htmlOutput .= ' <p class="text-slate-500 text-sm mt-0.5 leading-relaxed">' . e($prospek->deskripsi_pekerjaan) . '</p>';
return 'Lulusan bidang ini memiliki prospek sebagai guru, tenaga pendidik, instruktur pelatihan, pengembang kurikulum, maupun konsultan pendidikan.'; $htmlOutput .= '</li>';
} }
return 'Prospek karier untuk jurusan ini sangat luas, mulai dari spesialis industri, konsultan ahli, hingga pegawai instansi pemerintah/BUMN di sektor terkait.'; $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;
}
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';
} }
} catch (error) { Swal.fire('Error!', 'Gagal menghubungi server.', 'error'); }
finally { btn.disabled = false; }
}); });
</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>
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`;
const dbId = document.getElementById('alumni_id_db').value; const dbId = document.getElementById('alumni_id_db').value;
async function loadAlumni() { function getToken() {
try { return localStorage.getItem('access_token') ||
const response = await fetch(`/api/admin/alumni/${dbId}`, { localStorage.getItem('token') ||
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('access_token') } sessionStorage.getItem('access_token') ||
}); sessionStorage.getItem('token') ||
const result = await response.json(); '';
if (response.ok) {
document.getElementById('nama_alumni').value = result.data.nama_alumni;
document.getElementById('perguruan_tinggi').value = result.data.perguruan_tinggi;
document.getElementById('jurusan_diambil').value = result.data.jurusan_diambil;
document.getElementById('display_id_alumni').innerText = `ID Alumni: ${result.data.id_alumni}`;
}
} catch (error) { console.error(error); }
} }
document.getElementById('formEditAlumni').addEventListener('submit', async (e) => { 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;
}
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';
} }
} catch (error) { Swal.fire('Error!', 'Gagal update.', 'error'); }
finally { btn.disabled = false; }
}); });
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;
}
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';
} }
} catch (error) { Swal.fire('Error!', 'Gagal menghubungi server.', 'error'); }
finally { btn.disabled = false; }
}); });
</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>
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`;
const dbId = document.getElementById('guru_id_db').value; const dbId = document.getElementById('guru_id_db').value;
async function loadGuru() { function getToken() {
try { return localStorage.getItem('access_token') ||
const response = await fetch(`/api/admin/guru/${dbId}`, { localStorage.getItem('token') ||
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('access_token') } sessionStorage.getItem('access_token') ||
}); sessionStorage.getItem('token') ||
const result = await response.json(); '';
if (response.ok) {
document.getElementById('nama_guru').value = result.data.nama_guru;
document.getElementById('display_id_guru').innerText = `Ubah profil administrator guru BK (${result.data.id_guru}).`;
}
} catch (error) { console.error(error); }
} }
document.getElementById('formEditGuru').addEventListener('submit', async (e) => { 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;
}
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';
} }
} catch (error) { Swal.fire('Error!', 'Gagal koneksi.', 'error'); }
finally { btn.disabled = false; }
}); });
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;
}
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';
} }
} catch (error) { Swal.fire('Error!', 'Gagal menghubungi server.', 'error'); }
finally { btn.disabled = false; }
}); });
</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>
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`;
const jurusanId = document.getElementById('jurusan_id').value; const jurusanId = document.getElementById('jurusan_id').value;
async function loadData() { function getToken() {
try { return localStorage.getItem('access_token') ||
const response = await fetch(`/api/admin/jurusan/${jurusanId}`, { localStorage.getItem('token') ||
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('access_token') } sessionStorage.getItem('access_token') ||
}); sessionStorage.getItem('token') ||
const result = await response.json(); '';
if (response.ok) {
document.getElementById('nama_jurusan').value = result.data.nama_jurusan;
document.getElementById('rumpun').value = result.data.rumpun;
document.getElementById('prospek_karir').value = result.data.prospek_karir || '';
}
} catch (error) { console.error(error); }
} }
document.getElementById('formEditJurusan').addEventListener('submit', async (e) => { 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;
}
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';
} }
} catch (error) { Swal.fire('Error!', 'Gagal koneksi.', 'error'); }
finally { btn.disabled = false; }
}); });
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() {
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_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 { try {
const response = await fetch('/api/admin/jurusan', { return text ? JSON.parse(text) : {};
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('access_token') } } catch (error) {
}); return {
const result = await response.json(); 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 loadJurusan() {
const token = getToken();
const select = document.getElementById('jurusan_id'); 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>'; select.innerHTML = '<option value="" disabled selected>Pilih Jurusan Terkait...</option>';
result.data.forEach(item => { if (response.status === 401 || response.status === 403) {
select.innerHTML += `<option value="${item.id}">${item.nama_jurusan}</option>`; 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) { } catch (error) {
document.getElementById('jurusan_id').innerHTML = '<option disabled>Gagal memuat jurusan</option>'; 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 (e) => { 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;
}
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';
} }
} catch (error) { Swal.fire('Error!', 'Gagal menghubungi server.', 'error'); }
finally { btn.disabled = false; }
}); });
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>
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_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; const prospekId = document.getElementById('prospek_id').value;
async function loadJurusanAndData() { function getToken() {
const token = localStorage.getItem('access_token'); 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 { try {
const resJurusan = await fetch('/api/admin/jurusan', { return text ? JSON.parse(text) : {};
headers: { 'Authorization': 'Bearer ' + token } } catch (error) {
}); return {
const resultJurusan = await resJurusan.json(); 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 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>'; select.innerHTML = '<option value="" disabled>Pilih Jurusan Terkait...</option>';
resultJurusan.data.forEach(item => { if (response.status === 401 || response.status === 403) {
select.innerHTML += `<option value="${item.id}">${item.nama_jurusan}</option>`; 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 resProspek = await fetch(`/api/admin/prospek-kerja/${prospekId}`, { const resultProspek = await parseJsonResponse(resProspek);
headers: { 'Authorization': 'Bearer ' + token }
});
const resultProspek = await resProspek.json();
if (resProspek.ok) { if (resProspek.status === 401 || resProspek.status === 403) {
document.getElementById('jurusan_id').value = resultProspek.data.jurusan_id; window.location.href = LOGIN_URL;
document.getElementById('nama_profesi').value = resultProspek.data.nama_profesi; 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('range_gaji').value = resultProspek.data.range_gaji || '';
document.getElementById('deskripsi_pekerjaan').value = resultProspek.data.deskripsi_pekerjaan || ''; document.getElementById('deskripsi_pekerjaan').value = resultProspek.data.deskripsi_pekerjaan || '';
} catch (error) {
Swal.fire('Error!', error.message || 'Gagal memuat data prospek kerja.', 'error');
} }
} catch (error) { console.error(error); }
} }
document.getElementById('formEditProspek').addEventListener('submit', async (e) => { 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;
}
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';
} }
} catch (error) { Swal.fire('Error!', 'Gagal koneksi.', 'error'); }
finally { btn.disabled = false; }
}); });
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,17 +418,26 @@ 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;
}
if (!response.ok || result.success === false) {
throw new Error(result.message || 'Gagal memuat detail riwayat.');
}
const data = result.data || {};
modalBody.innerHTML = ` modalBody.innerHTML = `
<div class="detail-result-card"> <div class="detail-result-card">
@ -427,11 +492,12 @@ function renderObjectGrid(data, className) {
${renderObjectGrid(data.riasec, 'mini-data-item')} ${renderObjectGrid(data.riasec, 'mini-data-item')}
</div> </div>
`; `;
} else {
modalBody.innerHTML = `<div class="alert alert-danger m-0">Gagal memuat detail: ${escapeHtml(result.message)}</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,157 +21,117 @@
<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>
<div class="pagination-container">
<div class="text-muted small" id="pagination-info">
Menampilkan 0 sampai 0 dari 0 data
</div>
<div class="d-flex gap-1 flex-wrap" id="pagination-controls"></div>
</div>
</div> </div>
</div> </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>`;
}
} catch (error) {
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>`;
}
} }
const data = result.data || []; function renderTable(siswaData, currentPage, perPage) {
tbody.innerHTML = ''; const tableContent = document.getElementById('tableContent');
if (data.length === 0) { if (siswaData.length === 0) {
tbody.innerHTML = ` 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>';
<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; return;
} }
data.forEach((item, index) => { let html = `
tbody.innerHTML += ` <div class="table-responsive">
<table class="table table-hover align-middle table-modern">
<thead>
<tr> <tr>
<td class="text-center text-muted fw-bold">${(result.from || 1) + index}</td> <th width="5%" class="text-center">No</th>
<td class="text-center fw-bold text-muted">${escapeHtml(item.id_siswa)}</td> <th width="25%">Nama Lengkap</th>
<td> <th width="20%">NISN</th>
<div class="table-user-cell"> <th width="15%" class="text-center">Kelas</th>
<div class="avatar-circle">${getInitial(item.nama_lengkap)}</div> <th width="20%">Username</th>
<div> <th width="15%" class="text-center">Aksi</th>
<div class="fw-bold text-dark">${escapeHtml(item.nama_lengkap)}</div> </tr>
<div class="text-cell-muted">Data siswa aktif</div> </thead>
</div> <tbody>
</div> `;
</td>
<td class="text-muted fw-semibold">${escapeHtml(item.nisn)}</td> siswaData.forEach((siswa, index) => {
const no = (currentPage - 1) * perPage + (index + 1);
const editUrl = `{{ url('/admin/siswa') }}/${siswa.id}/edit`;
const username = siswa.user ? siswa.user.username : '<span class="text-danger small">Tidak ada akun</span>';
html += `
<tr>
<td class="text-center">${no}</td>
<td class="fw-bold">${siswa.nama_lengkap || '-'}</td>
<td><span class="badge bg-light text-dark border">${siswa.nisn || '-'}</span></td>
<td class="text-center"><span class="badge" style="background-color: #2D7A47;">${siswa.kelas || '-'}</span></td>
<td>${username}</td>
<td class="text-center"> <td class="text-center">
<span class="badge-soft badge-kelas">${escapeHtml(item.kelas)}</span> <div class="d-flex justify-content-center gap-2">
</td> <a href="${editUrl}" class="btn btn-sm btn-outline-primary shadow-sm" title="Edit Data">
<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> <i class="fas fa-edit"></i>
</a> </a>
<button onclick="deleteSiswa(${item.id})" class="btn-action btn-delete" title="Hapus"> <button onclick="deleteSiswa(${siswa.id})" class="btn btn-sm btn-outline-danger shadow-sm" title="Hapus Data">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</button> </button>
</div> </div>
@ -180,112 +140,68 @@ function showLoading() {
`; `;
}); });
updatePaginationInfo(result); html += `
renderPagination(result); </tbody>
</table>
} catch (error) { </div>
tbody.innerHTML = ` <div id="paginationWrapper" class="d-flex justify-content-between align-items-center p-3 border-top bg-light mt-3 rounded-bottom"></div>
<tr>
<td colspan="6" class="empty-state-modern text-danger">
<i class="fas fa-exclamation-circle"></i>
Gagal memuat data siswa.
</td>
</tr>
`; `;
}
tableContent.innerHTML = html;
} }
function updatePaginationInfo(data) { function renderPagination(meta) {
const info = document.getElementById('pagination-info'); const paginationWrapper = document.getElementById('paginationWrapper');
if (!paginationWrapper) return;
if (!data || !data.total) { let html = `<div class="text-muted small fw-bold">Menampilkan ${meta.from || 0} - ${meta.to || 0} dari ${meta.total || 0} Data</div>`;
info.innerText = 'Menampilkan 0 sampai 0 dari 0 data'; html += `<div class="d-flex gap-2">`;
return;
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>`;
} }
info.innerText = `Menampilkan ${data.from || 0} sampai ${data.to || 0} dari ${data.total || 0} data`; 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>`;
} }
function renderPagination(data) { html += `</div>`;
const controls = document.getElementById('pagination-controls'); paginationWrapper.innerHTML = html;
controls.innerHTML = '';
const current = Number(data.current_page || 1);
const last = Number(data.last_page || 1);
const total = Number(data.total || 0);
if (total === 0) return;
const createButton = (label, page, disabled = false, active = false) => {
const btn = document.createElement('button');
btn.className = `page-link-custom ${active ? 'active' : ''}`;
btn.innerHTML = label;
btn.disabled = disabled;
btn.onclick = () => {
if (!disabled && !active) fetchSiswa(page);
};
return btn;
};
controls.appendChild(createButton('<i class="fas fa-chevron-left"></i>', current - 1, current === 1));
let pages = [];
if (last <= 5) {
for (let i = 1; i <= last; i++) pages.push(i);
} else {
pages.push(1);
if (current > 3) pages.push('...');
const start = Math.max(2, current - 1);
const end = Math.min(last - 1, current + 1);
for (let i = start; i <= end; i++) pages.push(i);
if (current < last - 2) pages.push('...');
pages.push(last);
}
pages.forEach(page => {
if (page === '...') {
controls.appendChild(createButton('...', current, true));
} else {
controls.appendChild(createButton(page, page, false, page === current));
}
});
controls.appendChild(createButton('<i class="fas fa-chevron-right"></i>', current + 1, current === last));
} }
async function deleteSiswa(id) { 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) : {};
} catch (error) {
return { success: false, message: 'Respon server tidak valid.' };
}
}
document.addEventListener('DOMContentLoaded', async function () {
const id = document.getElementById('siswa_id').value;
const token = getToken();
if (!token) {
window.location.href = LOGIN_URL;
return;
}
try {
const response = await fetch(`${API_SISWA_URL}/${id}`, {
method: 'GET',
headers: { headers: {
'Authorization': 'Bearer ' + localStorage.getItem('access_token'), 'Authorization': 'Bearer ' + token,
'Accept': 'application/json' 'Accept': 'application/json'
} }
}); });
const result = await response.json();
if (response.ok) { const result = await parseJsonResponse(response);
document.getElementById('nama_lengkap').value = result.data.nama_lengkap;
document.getElementById('nisn').value = result.data.nisn; if (response.ok && result.success) {
document.getElementById('kelas').value = result.data.kelas; const s = result.data;
document.getElementById('peminatan').value = result.data.peminatan; document.getElementById('nama_lengkap').value = s.nama_lengkap || '';
document.getElementById('display_id_siswa').innerText = `EDITING: ${result.data.id_siswa}`; 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 { } else {
Swal.fire('Gagal!', 'Data tidak ditemukan.', 'warning'); Swal.fire('Gagal!', result.message || 'Gagal memuat rincian data siswa.', 'error');
} }
} catch (error) { } catch (error) {
Swal.fire('Error!', 'Gagal mengambil data dari server.', 'error'); Swal.fire('Error!', 'Gagal menghubungi server untuk memuat data.', 'error');
}
} }
});
// Mengirim data perbaikan ke API document.getElementById('formEditSiswa').addEventListener('submit', async function (e) {
document.getElementById('editSiswaForm').addEventListener('submit', async (e) => {
e.preventDefault(); e.preventDefault();
const id = document.getElementById('siswa_id').value;
const btn = document.getElementById('btnUpdate'); const btn = document.getElementById('btnUpdate');
btn.disabled = true; btn.disabled = true;
btn.innerText = 'Memperbarui...';
const payload = { const payload = {
nama_lengkap: document.getElementById('nama_lengkap').value, nama_lengkap: document.getElementById('nama_lengkap').value.trim(),
nisn: document.getElementById('nisn').value, nisn: document.getElementById('nisn').value.trim(),
kelas: document.getElementById('kelas').value, kelas: document.getElementById('kelas').value
peminatan: document.getElementById('peminatan').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 { try {
const response = await fetch(`/api/admin/siswa/${dbId}`, { 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,22 +48,9 @@
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 class="flex items-center gap-3">
<div class="hidden sm:flex items-center gap-3 bg-emerald-50 px-4 py-2 rounded-full border border-emerald-100">
<div class="w-2 h-2 bg-emerald-500 rounded-full animate-pulse"></div>
<span class="text-[10px] font-bold text-emerald-700 uppercase">
Siswa
</span>
</div> </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"> <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">
@ -136,7 +59,6 @@
</span> </span>
</button> </button>
</div> </div>
</div>
</nav> </nav>
<main class="flex-grow px-6 py-10"> <main class="flex-grow px-6 py-10">
@ -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
@foreach ($mapel as $id => $label)
<div> <div>
<label class="block text-xs font-bold text-slate-500 mb-2">PKN</label> <label class="block text-xs font-bold text-slate-500 mb-2">
<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 }}
</div> </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>
<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>
@endforeach
</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 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> </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]); } catch (error) {
console.error('Logout error:', error);
if (isNaN(value) || value < 0 || value > 100) {
return `Nilai ${field.replaceAll('_', ' ')} harus berada pada rentang 0 sampai 100.`;
} }
} }
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 = '';
for (const id of questionIds) { alertBox.classList.add('hidden');
if (!document.getElementById(id).value) {
return 'Semua pertanyaan minat dan bakat wajib diisi.';
}
} }
return null; function getNumberValue(id) {
const value = document.getElementById(id).value;
return value === '' ? null : Number(value);
} }
function calculateRiasecScores() { 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'
} }
});
} catch (error) { const value = Number(payload[field]);
console.error('Logout error:', error);
if (Number.isNaN(value) || value < 0 || value > 100) {
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');
}); });