upload all changes in project sirekom
This commit is contained in:
parent
f1d9460598
commit
0d8a13d3ac
|
|
@ -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>
|
||||
|
|
@ -16,7 +16,7 @@ public function create()
|
|||
return view('admin.siswa.siswacreate');
|
||||
}
|
||||
|
||||
public function edit($id)
|
||||
public function edit($id)
|
||||
{
|
||||
return view('admin.siswa.siswaedit', compact('id'));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,102 +6,195 @@
|
|||
use App\Models\Alumni;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Throwable;
|
||||
|
||||
class AlumniController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$perPage = (int) $request->input('per_page', 7);
|
||||
$perPage = in_array($perPage, [5, 7]) ? $perPage : 7;
|
||||
try {
|
||||
$perPage = (int) $request->input('per_page', 7);
|
||||
$perPage = in_array($perPage, [5, 7, 10, 25, 50, 100]) ? $perPage : 7;
|
||||
|
||||
$search = trim((string) $request->input('search', ''));
|
||||
$search = trim((string) $request->input('search', ''));
|
||||
|
||||
$query = Alumni::query()->orderBy('id', 'desc');
|
||||
$query = Alumni::query()->orderBy('id', 'desc');
|
||||
|
||||
if ($search !== '') {
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('id_alumni', 'like', "%{$search}%")
|
||||
->orWhere('nama_alumni', 'like', "%{$search}%")
|
||||
->orWhere('perguruan_tinggi', 'like', "%{$search}%")
|
||||
->orWhere('jurusan_diambil', 'like', "%{$search}%");
|
||||
});
|
||||
if ($search !== '') {
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('id_alumni', 'like', "%{$search}%")
|
||||
->orWhere('nama_alumni', 'like', "%{$search}%")
|
||||
->orWhere('perguruan_tinggi', 'like', "%{$search}%")
|
||||
->orWhere('jurusan_diambil', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
$alumni = $query->paginate($perPage);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Data alumni berhasil dimuat.',
|
||||
'data' => $alumni->items(),
|
||||
'current_page' => $alumni->currentPage(),
|
||||
'last_page' => $alumni->lastPage(),
|
||||
'per_page' => $alumni->perPage(),
|
||||
'total' => $alumni->total(),
|
||||
'from' => $alumni->firstItem(),
|
||||
'to' => $alumni->lastItem(),
|
||||
], 200);
|
||||
} catch (Throwable $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Gagal memuat data alumni.',
|
||||
'error' => $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
|
||||
$alumni = $query->paginate($perPage);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $alumni->items(),
|
||||
'current_page' => $alumni->currentPage(),
|
||||
'last_page' => $alumni->lastPage(),
|
||||
'per_page' => $alumni->perPage(),
|
||||
'total' => $alumni->total(),
|
||||
'from' => $alumni->firstItem(),
|
||||
'to' => $alumni->lastItem(),
|
||||
], 200);
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'nama_alumni' => 'required|string|max:255',
|
||||
'perguruan_tinggi' => 'required|string',
|
||||
'jurusan_diambil' => 'required|string',
|
||||
], [
|
||||
'nama_alumni.required' => 'Nama alumni wajib diisi.',
|
||||
'perguruan_tinggi.required' => 'Nama Perguruan Tinggi harus diisi.',
|
||||
'jurusan_diambil.required' => 'Jurusan yang diambil tidak boleh kosong.',
|
||||
]);
|
||||
try {
|
||||
$validator = Validator::make($request->all(), [
|
||||
'nama_alumni' => 'required|string|max:255',
|
||||
'perguruan_tinggi' => 'required|string|max:255',
|
||||
'jurusan_diambil' => 'required|string|max:255',
|
||||
], [
|
||||
'nama_alumni.required' => 'Nama alumni wajib diisi.',
|
||||
'perguruan_tinggi.required' => 'Nama perguruan tinggi wajib diisi.',
|
||||
'jurusan_diambil.required' => 'Jurusan yang diambil wajib diisi.',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Validasi gagal.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$alumni = new Alumni();
|
||||
$alumni->nama_alumni = $request->input('nama_alumni');
|
||||
$alumni->perguruan_tinggi = $request->input('perguruan_tinggi');
|
||||
$alumni->jurusan_diambil = $request->input('jurusan_diambil');
|
||||
$alumni->profil_nilai = $request->input('profil_nilai');
|
||||
$alumni->profil_minat = $request->input('profil_minat');
|
||||
$alumni->save();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Data alumni berhasil ditambahkan.',
|
||||
'data' => $alumni,
|
||||
], 201);
|
||||
} catch (Throwable $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Terjadi kesalahan saat menyimpan data alumni.',
|
||||
'error' => $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
$alumni = Alumni::create($request->all());
|
||||
public function show($id)
|
||||
{
|
||||
try {
|
||||
$alumni = Alumni::find($id);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Alumni ' . $alumni->id_alumni . ' berhasil ditambahkan.',
|
||||
'data' => $alumni
|
||||
], 201);
|
||||
if (!$alumni) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Data alumni tidak ditemukan.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Data alumni berhasil ditemukan.',
|
||||
'data' => $alumni,
|
||||
], 200);
|
||||
} catch (Throwable $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Terjadi kesalahan saat mengambil data alumni.',
|
||||
'error' => $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
$alumni = Alumni::find($id);
|
||||
try {
|
||||
$alumni = Alumni::find($id);
|
||||
|
||||
if (!$alumni) {
|
||||
return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
|
||||
if (!$alumni) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Data alumni tidak ditemukan.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'nama_alumni' => 'required|string|max:255',
|
||||
'perguruan_tinggi' => 'required|string|max:255',
|
||||
'jurusan_diambil' => 'required|string|max:255',
|
||||
], [
|
||||
'nama_alumni.required' => 'Nama alumni wajib diisi.',
|
||||
'perguruan_tinggi.required' => 'Nama perguruan tinggi wajib diisi.',
|
||||
'jurusan_diambil.required' => 'Jurusan yang diambil wajib diisi.',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Validasi gagal.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$alumni->nama_alumni = $request->input('nama_alumni');
|
||||
$alumni->perguruan_tinggi = $request->input('perguruan_tinggi');
|
||||
$alumni->jurusan_diambil = $request->input('jurusan_diambil');
|
||||
$alumni->profil_nilai = $request->input('profil_nilai');
|
||||
$alumni->profil_minat = $request->input('profil_minat');
|
||||
$alumni->save();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Data alumni berhasil diperbarui.',
|
||||
'data' => $alumni,
|
||||
], 200);
|
||||
} catch (Throwable $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Terjadi kesalahan saat memperbarui data alumni.',
|
||||
'error' => $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'nama_alumni' => 'required|string|max:255',
|
||||
'perguruan_tinggi' => 'required|string',
|
||||
'jurusan_diambil' => 'required|string',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
|
||||
}
|
||||
|
||||
$alumni->update($request->all());
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Data ' . $alumni->id_alumni . ' berhasil diperbarui'
|
||||
], 200);
|
||||
}
|
||||
|
||||
public function destroy($id)
|
||||
{
|
||||
$alumni = Alumni::find($id);
|
||||
try {
|
||||
$alumni = Alumni::find($id);
|
||||
|
||||
if (!$alumni) {
|
||||
return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
|
||||
if (!$alumni) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Data alumni tidak ditemukan.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
$alumni->delete();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Data alumni berhasil dihapus.',
|
||||
], 200);
|
||||
} catch (Throwable $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Data alumni tidak dapat dihapus karena masih digunakan atau terjadi kesalahan server.',
|
||||
'error' => $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
|
||||
$alumni->delete();
|
||||
|
||||
return response()->json(['success' => true, 'message' => 'Data Alumni berhasil dihapus'], 200);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,40 +3,41 @@
|
|||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use App\Models\RiwayatRekomendasi;
|
||||
use App\Models\Siswa;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Exception;
|
||||
use Throwable;
|
||||
|
||||
class DashboardAdminController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
try {
|
||||
$tableName = (new RiwayatRekomendasi())->getTable();
|
||||
$riwayatModel = new RiwayatRekomendasi();
|
||||
$tableName = $riwayatModel->getTable();
|
||||
|
||||
$totalSiswa = User::whereIn('role', ['siswa', 'student'])->count();
|
||||
$totalSiswa = 0;
|
||||
|
||||
try {
|
||||
$siswaModel = new Siswa();
|
||||
if (Schema::hasTable($siswaModel->getTable())) {
|
||||
$totalSiswa = Siswa::count();
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$totalSiswa = 0;
|
||||
}
|
||||
|
||||
if ($totalSiswa === 0) {
|
||||
$totalSiswa = RiwayatRekomendasi::distinct('user_id')->count('user_id');
|
||||
}
|
||||
|
||||
$totalRekomendasi = RiwayatRekomendasi::count();
|
||||
|
||||
$kolomAkurasi = null;
|
||||
|
||||
if (Schema::hasColumn($tableName, 'akurasi_prediksi')) {
|
||||
$kolomAkurasi = 'akurasi_prediksi';
|
||||
} elseif (Schema::hasColumn($tableName, 'probabilitas')) {
|
||||
$kolomAkurasi = 'probabilitas';
|
||||
} elseif (Schema::hasColumn($tableName, 'akurasi')) {
|
||||
$kolomAkurasi = 'akurasi';
|
||||
$totalSiswa = User::whereIn('role', ['siswa', 'student'])->count();
|
||||
}
|
||||
|
||||
$totalRekomendasi = Schema::hasTable($tableName) ? RiwayatRekomendasi::count() : 0;
|
||||
$kolomAkurasi = $this->getKolomAkurasi($tableName);
|
||||
$rataAkurasi = null;
|
||||
|
||||
if ($kolomAkurasi) {
|
||||
if ($kolomAkurasi && Schema::hasTable($tableName)) {
|
||||
$avg = RiwayatRekomendasi::whereNotNull($kolomAkurasi)->avg($kolomAkurasi);
|
||||
$rataAkurasi = $avg !== null ? round((float) $avg, 2) : null;
|
||||
}
|
||||
|
|
@ -45,44 +46,64 @@ public function index(Request $request)
|
|||
? 'Berdasarkan rata-rata tingkat keyakinan dari seluruh hasil rekomendasi.'
|
||||
: 'Belum ada nilai keyakinan prediksi yang tersimpan. Lakukan prediksi baru terlebih dahulu.';
|
||||
|
||||
$selectDistribusi = 'hasil_rekomendasi_jurusan as jurusan, COUNT(*) as total';
|
||||
$distribusiJurusan = collect();
|
||||
|
||||
if ($kolomAkurasi) {
|
||||
$selectDistribusi .= ', ROUND(AVG(' . $kolomAkurasi . '), 2) as rata_akurasi';
|
||||
if (Schema::hasTable($tableName) && Schema::hasColumn($tableName, 'hasil_rekomendasi_jurusan')) {
|
||||
$selectDistribusi = 'hasil_rekomendasi_jurusan as jurusan, COUNT(*) as total';
|
||||
|
||||
if ($kolomAkurasi) {
|
||||
$selectDistribusi .= ', ROUND(AVG(' . $kolomAkurasi . '), 2) as rata_akurasi';
|
||||
}
|
||||
|
||||
$distribusiJurusan = RiwayatRekomendasi::selectRaw($selectDistribusi)
|
||||
->whereNotNull('hasil_rekomendasi_jurusan')
|
||||
->groupBy('hasil_rekomendasi_jurusan')
|
||||
->orderBy('total', 'desc')
|
||||
->limit(10)
|
||||
->get()
|
||||
->map(function ($item) use ($kolomAkurasi) {
|
||||
return [
|
||||
'jurusan' => $item->jurusan,
|
||||
'total' => (int) $item->total,
|
||||
'rata_akurasi' => $kolomAkurasi && $item->rata_akurasi !== null
|
||||
? round((float) $item->rata_akurasi, 2)
|
||||
: null,
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
$distribusiJurusan = RiwayatRekomendasi::selectRaw($selectDistribusi)
|
||||
->whereNotNull('hasil_rekomendasi_jurusan')
|
||||
->groupBy('hasil_rekomendasi_jurusan')
|
||||
->orderBy('total', 'desc')
|
||||
->limit(10)
|
||||
->get()
|
||||
->map(function ($item) use ($kolomAkurasi) {
|
||||
return [
|
||||
'jurusan' => $item->jurusan,
|
||||
'total' => (int) $item->total,
|
||||
'rata_akurasi' => $kolomAkurasi && $item->rata_akurasi !== null
|
||||
? round((float) $item->rata_akurasi, 2)
|
||||
: null,
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Statistik dashboard berhasil dimuat.',
|
||||
'data' => [
|
||||
'total_siswa' => $totalSiswa,
|
||||
'rekomendasi_diproses' => $totalRekomendasi,
|
||||
'rata_rata_akurasi' => $rataAkurasi,
|
||||
'akurasi_status' => $akurasiStatus,
|
||||
'distribusi_jurusan' => $distribusiJurusan,
|
||||
]
|
||||
],
|
||||
], 200);
|
||||
|
||||
} catch (Exception $e) {
|
||||
} catch (Throwable $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Error: ' . $e->getMessage()
|
||||
'message' => 'Gagal memuat statistik dashboard.',
|
||||
'error' => $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
private function getKolomAkurasi(string $tableName): ?string
|
||||
{
|
||||
if (!Schema::hasTable($tableName)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (['akurasi_prediksi', 'probabilitas', 'akurasi'] as $column) {
|
||||
if (Schema::hasColumn($tableName, $column)) {
|
||||
return $column;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -4,98 +4,242 @@
|
|||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Guru;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Throwable;
|
||||
|
||||
class GuruController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$perPage = (int) $request->input('per_page', 7);
|
||||
$perPage = in_array($perPage, [5, 7]) ? $perPage : 7;
|
||||
try {
|
||||
$perPage = (int) $request->input('per_page', 7);
|
||||
$perPage = in_array($perPage, [5, 7, 10, 25]) ? $perPage : 7;
|
||||
|
||||
$search = trim((string) $request->input('search', ''));
|
||||
$search = trim((string) $request->input('search', ''));
|
||||
|
||||
$query = Guru::query()->orderBy('id', 'desc');
|
||||
$query = Guru::query()->with('user')->orderBy('id', 'desc');
|
||||
|
||||
if ($search !== '') {
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('id_guru', 'like', "%{$search}%")
|
||||
->orWhere('nama_guru', 'like', "%{$search}%");
|
||||
});
|
||||
if ($search !== '') {
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('id_guru', 'like', "%{$search}%")
|
||||
->orWhere('nama_guru', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
$guru = $query->paginate($perPage);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Data guru berhasil dimuat.',
|
||||
'data' => $guru->items(),
|
||||
'current_page' => $guru->currentPage(),
|
||||
'last_page' => $guru->lastPage(),
|
||||
'per_page' => $guru->perPage(),
|
||||
'total' => $guru->total(),
|
||||
], 200);
|
||||
|
||||
} catch (Throwable $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Terjadi kesalahan saat memuat data guru.',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
|
||||
$guru = $query->paginate($perPage);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $guru->items(),
|
||||
'current_page' => $guru->currentPage(),
|
||||
'last_page' => $guru->lastPage(),
|
||||
'per_page' => $guru->perPage(),
|
||||
'total' => $guru->total(),
|
||||
'from' => $guru->firstItem(),
|
||||
'to' => $guru->lastItem(),
|
||||
], 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'nama_guru' => 'required|string|max:255',
|
||||
], [
|
||||
'nama_guru.required' => 'Nama guru tidak boleh kosong.',
|
||||
'username' => 'required|string|max:255|unique:users,username',
|
||||
'email' => 'required|string|email|max:255|unique:users,email',
|
||||
'password' => 'required|string|min:8',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Validasi gagal.',
|
||||
'errors' => $validator->errors()
|
||||
], 422);
|
||||
}
|
||||
|
||||
$guru = Guru::create($request->all());
|
||||
DB::beginTransaction();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Guru ' . $guru->id_guru . ' berhasil ditambahkan.',
|
||||
'data' => $guru
|
||||
], 201);
|
||||
try {
|
||||
// Create User Akun
|
||||
$user = User::create([
|
||||
'name' => $request->nama_guru,
|
||||
'username' => $request->username,
|
||||
'email' => $request->email,
|
||||
'password' => Hash::make($request->password),
|
||||
'role' => 'admin',
|
||||
]);
|
||||
|
||||
// Create Data Guru terikat dengan user_id
|
||||
$guru = Guru::create([
|
||||
'user_id' => $user->id,
|
||||
'nama_guru' => $request->nama_guru,
|
||||
]);
|
||||
|
||||
DB::commit();
|
||||
|
||||
$guru->load('user');
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Data guru dan akun login berhasil disimpan.',
|
||||
'data' => $guru
|
||||
], 201);
|
||||
|
||||
} catch (Throwable $e) {
|
||||
DB::rollBack();
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Terjadi kesalahan saat menyimpan data guru.',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*/
|
||||
public function show($id)
|
||||
{
|
||||
try {
|
||||
$guru = Guru::with('user')->find($id);
|
||||
|
||||
if (!$guru) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Data guru tidak ditemukan.'
|
||||
], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Detail data guru berhasil ditemukan.',
|
||||
'data' => $guru
|
||||
], 200);
|
||||
|
||||
} catch (Throwable $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Terjadi kesalahan saat mengambil detail data guru.',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
$guru = Guru::find($id);
|
||||
try {
|
||||
$guru = Guru::find($id);
|
||||
|
||||
if (!$guru) {
|
||||
return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
|
||||
if (!$guru) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Data guru tidak ditemukan.'
|
||||
], 404);
|
||||
}
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'nama_guru' => 'required|string|max:255',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Validasi gagal.',
|
||||
'errors' => $validator->errors()
|
||||
], 422);
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
$guru->update([
|
||||
'nama_guru' => $request->nama_guru
|
||||
]);
|
||||
|
||||
if ($guru->user) {
|
||||
$guru->user->update([
|
||||
'name' => $request->nama_guru
|
||||
]);
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
$guru->load('user');
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Data guru berhasil diperbarui.',
|
||||
'data' => $guru
|
||||
], 200);
|
||||
|
||||
} catch (Throwable $e) {
|
||||
DB::rollBack();
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Terjadi kesalahan saat memperbarui data guru.',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'nama_guru' => 'required|string|max:255',
|
||||
], [
|
||||
'nama_guru.required' => 'Update gagal, nama guru harus diisi.',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
|
||||
}
|
||||
|
||||
$guru->update($request->all());
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Data ' . $guru->id_guru . ' berhasil diperbarui'
|
||||
], 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*/
|
||||
public function destroy($id)
|
||||
{
|
||||
$guru = Guru::find($id);
|
||||
try {
|
||||
$guru = Guru::find($id);
|
||||
|
||||
if (!$guru) {
|
||||
return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
|
||||
if (!$guru) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Data guru tidak ditemukan.'
|
||||
], 404);
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
if ($guru->user_id) {
|
||||
User::destroy($guru->user_id);
|
||||
}
|
||||
|
||||
$guru->delete();
|
||||
|
||||
DB::commit();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Data guru dan akun login berhasil dihapus.'
|
||||
], 200);
|
||||
|
||||
} catch (Throwable $e) {
|
||||
DB::rollBack();
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Data guru tidak dapat dihapus atau terjadi kesalahan server.',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
|
||||
$guru->delete();
|
||||
|
||||
return response()->json(['success' => true, 'message' => 'Data Guru berhasil dihapus'], 200);
|
||||
}
|
||||
}
|
||||
|
|
@ -6,98 +6,191 @@
|
|||
use App\Models\Jurusan;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Throwable;
|
||||
|
||||
class JurusanController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$perPage = (int) $request->input('per_page', 7);
|
||||
$perPage = in_array($perPage, [5, 7]) ? $perPage : 7;
|
||||
try {
|
||||
$perPage = (int) $request->input('per_page', 7);
|
||||
$perPage = in_array($perPage, [5, 7, 10, 25, 50, 100]) ? $perPage : 7;
|
||||
|
||||
$search = trim((string) $request->input('search', ''));
|
||||
$search = trim((string) $request->input('search', ''));
|
||||
|
||||
$query = Jurusan::query()->orderBy('id', 'desc');
|
||||
// Mengambil data jurusan beserta semua prospek kerja yang terkait dengannya
|
||||
$query = Jurusan::with('prospeks')->orderBy('id', 'desc');
|
||||
|
||||
if ($search !== '') {
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('nama_jurusan', 'like', "%{$search}%")
|
||||
->orWhere('rumpun', 'like', "%{$search}%")
|
||||
->orWhere('prospek_karir', 'like', "%{$search}%");
|
||||
});
|
||||
if ($search !== '') {
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('nama_jurusan', 'like', "%{$search}%")
|
||||
->orWhere('rumpun', 'like', "%{$search}%")
|
||||
// Memperbaiki pencarian agar mencari ke dalam relasi tabel prospeks
|
||||
->orWhereHas('prospeks', function ($relationQuery) use ($search) {
|
||||
$relationQuery->where('nama_profesi', 'like', "%{$search}%");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$jurusan = $query->paginate($perPage);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Data jurusan berhasil dimuat.',
|
||||
'data' => $jurusan->items(),
|
||||
'current_page' => $jurusan->currentPage(),
|
||||
'last_page' => $jurusan->lastPage(),
|
||||
'per_page' => $jurusan->perPage(),
|
||||
'total' => $jurusan->total(),
|
||||
'from' => $jurusan->firstItem(),
|
||||
'to' => $jurusan->lastItem(),
|
||||
], 200);
|
||||
} catch (Throwable $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Terjadi kesalahan saat memuat data jurusan.',
|
||||
'error' => $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
|
||||
$jurusan = $query->paginate($perPage);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $jurusan->items(),
|
||||
'current_page' => $jurusan->currentPage(),
|
||||
'last_page' => $jurusan->lastPage(),
|
||||
'per_page' => $jurusan->perPage(),
|
||||
'total' => $jurusan->total(),
|
||||
'from' => $jurusan->firstItem(),
|
||||
'to' => $jurusan->lastItem(),
|
||||
], 200);
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'nama_jurusan' => 'required|string|max:255',
|
||||
'rumpun' => 'required|string',
|
||||
]);
|
||||
try {
|
||||
$validator = Validator::make($request->all(), [
|
||||
'nama_jurusan' => 'required|string|max:255',
|
||||
'rumpun' => ['required', 'string', Rule::in(['Saintek', 'Soshum'])],
|
||||
], [
|
||||
'nama_jurusan.required' => 'Nama jurusan wajib diisi.',
|
||||
'rumpun.required' => 'Rumpun jurusan wajib dipilih.',
|
||||
'rumpun.in' => 'Rumpun hanya boleh Saintek atau Soshum.',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Validasi gagal.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$jurusan = new Jurusan();
|
||||
$jurusan->nama_jurusan = $request->input('nama_jurusan');
|
||||
$jurusan->rumpun = $request->input('rumpun');
|
||||
$jurusan->save();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Data jurusan berhasil ditambahkan.',
|
||||
'data' => $jurusan,
|
||||
], 201);
|
||||
} catch (Throwable $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Terjadi kesalahan saat menyimpan data jurusan.',
|
||||
'error' => $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
|
||||
$jurusan = Jurusan::create($request->all());
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Jurusan berhasil ditambahkan',
|
||||
'data' => $jurusan
|
||||
], 201);
|
||||
}
|
||||
|
||||
public function show($id)
|
||||
{
|
||||
$jurusan = Jurusan::find($id);
|
||||
try {
|
||||
$jurusan = Jurusan::find($id);
|
||||
|
||||
if (!$jurusan) {
|
||||
return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
|
||||
if (!$jurusan) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Data jurusan tidak ditemukan.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Data jurusan berhasil ditemukan.',
|
||||
'data' => $jurusan,
|
||||
], 200);
|
||||
} catch (Throwable $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Terjadi kesalahan saat mengambil data jurusan.',
|
||||
'error' => $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
|
||||
return response()->json(['success' => true, 'data' => $jurusan], 200);
|
||||
}
|
||||
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
$jurusan = Jurusan::find($id);
|
||||
try {
|
||||
$jurusan = Jurusan::find($id);
|
||||
|
||||
if (!$jurusan) {
|
||||
return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
|
||||
if (!$jurusan) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Data jurusan tidak ditemukan.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'nama_jurusan' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:255',
|
||||
Rule::unique('jurusans', 'nama_jurusan')->ignore($id),
|
||||
],
|
||||
'rumpun' => 'required|in:Saintek,Soshum',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Validasi gagal.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$jurusan->nama_jurusan = $request->input('nama_jurusan');
|
||||
$jurusan->rumpun = $request->input('rumpun');
|
||||
$jurusan->save();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Data jurusan berhasil diperbarui.',
|
||||
'data' => $jurusan,
|
||||
], 200);
|
||||
} catch (Throwable $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Terjadi kesalahan saat memperbarui data jurusan.',
|
||||
'error' => $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
|
||||
$jurusan->update($request->all());
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Jurusan berhasil diperbarui',
|
||||
'data' => $jurusan
|
||||
], 200);
|
||||
}
|
||||
|
||||
public function destroy($id)
|
||||
{
|
||||
$jurusan = Jurusan::find($id);
|
||||
try {
|
||||
$jurusan = Jurusan::find($id);
|
||||
|
||||
if (!$jurusan) {
|
||||
return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
|
||||
if (!$jurusan) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Data jurusan tidak ditemukan.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
$jurusan->delete();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Data jurusan berhasil dihapus.',
|
||||
], 200);
|
||||
} catch (Throwable $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Data jurusan tidak dapat dihapus karena masih digunakan pada data lain atau terjadi kesalahan server.',
|
||||
'error' => $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
|
||||
$jurusan->delete();
|
||||
|
||||
return response()->json(['success' => true, 'message' => 'Jurusan berhasil dihapus'], 200);
|
||||
}
|
||||
}
|
||||
|
|
@ -6,114 +6,199 @@
|
|||
use App\Models\ProspekKerja;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Throwable;
|
||||
|
||||
class ProspekKerjaController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$perPage = (int) $request->input('per_page', 7);
|
||||
$perPage = in_array($perPage, [5, 7]) ? $perPage : 7;
|
||||
try {
|
||||
$perPage = (int) $request->input('per_page', 7);
|
||||
$perPage = in_array($perPage, [5, 7, 10, 25, 50, 100]) ? $perPage : 7;
|
||||
|
||||
$search = trim((string) $request->input('search', ''));
|
||||
$search = trim((string) $request->input('search', ''));
|
||||
|
||||
$query = ProspekKerja::with('jurusan')->orderBy('id', 'desc');
|
||||
$query = ProspekKerja::with('jurusan')->orderBy('id', 'desc');
|
||||
|
||||
if ($search !== '') {
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('nama_profesi', 'like', "%{$search}%")
|
||||
->orWhere('range_gaji', 'like', "%{$search}%")
|
||||
->orWhere('deskripsi_pekerjaan', 'like', "%{$search}%")
|
||||
->orWhereHas('jurusan', function ($jurusan) use ($search) {
|
||||
$jurusan->where('nama_jurusan', 'like', "%{$search}%");
|
||||
});
|
||||
});
|
||||
if ($search !== '') {
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('nama_profesi', 'like', "%{$search}%")
|
||||
->orWhere('range_gaji', 'like', "%{$search}%")
|
||||
->orWhere('deskripsi_pekerjaan', 'like', "%{$search}%")
|
||||
->orWhereHas('jurusan', function ($jurusan) use ($search) {
|
||||
$jurusan->where('nama_jurusan', 'like', "%{$search}%");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$prospek = $query->paginate($perPage);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Data prospek kerja berhasil dimuat.',
|
||||
'data' => $prospek->items(),
|
||||
'current_page' => $prospek->currentPage(),
|
||||
'last_page' => $prospek->lastPage(),
|
||||
'per_page' => $prospek->perPage(),
|
||||
'total' => $prospek->total(),
|
||||
'from' => $prospek->firstItem(),
|
||||
'to' => $prospek->lastItem(),
|
||||
], 200);
|
||||
} catch (Throwable $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Gagal memuat data prospek kerja.',
|
||||
'error' => $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
|
||||
$prospek = $query->paginate($perPage);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $prospek->items(),
|
||||
'current_page' => $prospek->currentPage(),
|
||||
'last_page' => $prospek->lastPage(),
|
||||
'per_page' => $prospek->perPage(),
|
||||
'total' => $prospek->total(),
|
||||
'from' => $prospek->firstItem(),
|
||||
'to' => $prospek->lastItem(),
|
||||
], 200);
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'jurusan_id' => 'required|exists:jurusans,id',
|
||||
'nama_profesi' => 'required|string|max:255',
|
||||
'range_gaji' => 'nullable|string|max:255',
|
||||
'deskripsi_pekerjaan' => 'nullable|string',
|
||||
]);
|
||||
try {
|
||||
$validator = Validator::make($request->all(), [
|
||||
'jurusan_id' => 'required|integer|exists:jurusans,id',
|
||||
'nama_profesi' => 'required|string|max:255',
|
||||
'range_gaji' => 'nullable|string|max:255',
|
||||
'deskripsi_pekerjaan' => 'nullable|string',
|
||||
], [
|
||||
'jurusan_id.required' => 'Jurusan wajib dipilih.',
|
||||
'jurusan_id.exists' => 'Jurusan yang dipilih tidak ditemukan.',
|
||||
'nama_profesi.required' => 'Nama profesi wajib diisi.',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Validasi gagal.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$prospek = new ProspekKerja();
|
||||
$prospek->jurusan_id = $request->input('jurusan_id');
|
||||
$prospek->nama_profesi = $request->input('nama_profesi');
|
||||
$prospek->range_gaji = $request->input('range_gaji');
|
||||
$prospek->deskripsi_pekerjaan = $request->input('deskripsi_pekerjaan');
|
||||
$prospek->save();
|
||||
$prospek->load('jurusan');
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Data prospek kerja berhasil ditambahkan.',
|
||||
'data' => $prospek,
|
||||
], 201);
|
||||
} catch (Throwable $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Terjadi kesalahan saat menyimpan data prospek kerja.',
|
||||
'error' => $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
|
||||
$prospek = ProspekKerja::create($request->all());
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Prospek Kerja berhasil ditambahkan',
|
||||
'data' => $prospek
|
||||
], 201);
|
||||
}
|
||||
|
||||
public function show($id)
|
||||
{
|
||||
$prospek = ProspekKerja::find($id);
|
||||
try {
|
||||
$prospek = ProspekKerja::with('jurusan')->find($id);
|
||||
|
||||
if (!$prospek) {
|
||||
return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
|
||||
if (!$prospek) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Data prospek kerja tidak ditemukan.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Data prospek kerja berhasil ditemukan.',
|
||||
'data' => $prospek,
|
||||
], 200);
|
||||
} catch (Throwable $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Terjadi kesalahan saat mengambil data prospek kerja.',
|
||||
'error' => $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
|
||||
return response()->json(['success' => true, 'data' => $prospek], 200);
|
||||
}
|
||||
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
$prospek = ProspekKerja::find($id);
|
||||
try {
|
||||
$prospek = ProspekKerja::find($id);
|
||||
|
||||
if (!$prospek) {
|
||||
return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
|
||||
if (!$prospek) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Data prospek kerja tidak ditemukan.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'jurusan_id' => 'required|integer|exists:jurusans,id',
|
||||
'nama_profesi' => 'required|string|max:255',
|
||||
'range_gaji' => 'nullable|string|max:255',
|
||||
'deskripsi_pekerjaan' => 'nullable|string',
|
||||
], [
|
||||
'jurusan_id.required' => 'Jurusan wajib dipilih.',
|
||||
'jurusan_id.exists' => 'Jurusan yang dipilih tidak ditemukan.',
|
||||
'nama_profesi.required' => 'Nama profesi wajib diisi.',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Validasi gagal.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$prospek->jurusan_id = $request->input('jurusan_id');
|
||||
$prospek->nama_profesi = $request->input('nama_profesi');
|
||||
$prospek->range_gaji = $request->input('range_gaji');
|
||||
$prospek->deskripsi_pekerjaan = $request->input('deskripsi_pekerjaan');
|
||||
$prospek->save();
|
||||
$prospek->load('jurusan');
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Data prospek kerja berhasil diperbarui.',
|
||||
'data' => $prospek,
|
||||
], 200);
|
||||
} catch (Throwable $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Terjadi kesalahan saat memperbarui data prospek kerja.',
|
||||
'error' => $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'jurusan_id' => 'required|exists:jurusans,id',
|
||||
'nama_profesi' => 'required|string|max:255',
|
||||
'range_gaji' => 'nullable|string|max:255',
|
||||
'deskripsi_pekerjaan' => 'nullable|string',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
|
||||
}
|
||||
|
||||
$prospek->update($request->all());
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Prospek Kerja berhasil diperbarui',
|
||||
'data' => $prospek
|
||||
], 200);
|
||||
}
|
||||
|
||||
public function destroy($id)
|
||||
{
|
||||
$prospek = ProspekKerja::find($id);
|
||||
try {
|
||||
$prospek = ProspekKerja::find($id);
|
||||
|
||||
if (!$prospek) {
|
||||
return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
|
||||
if (!$prospek) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Data prospek kerja tidak ditemukan.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
$prospek->delete();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Data prospek kerja berhasil dihapus.',
|
||||
], 200);
|
||||
} catch (Throwable $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Data prospek kerja tidak dapat dihapus karena masih digunakan atau terjadi kesalahan server.',
|
||||
'error' => $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
|
||||
$prospek->delete();
|
||||
|
||||
return response()->json(['success' => true, 'message' => 'Prospek Kerja berhasil dihapus'], 200);
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
use App\Models\RiwayatRekomendasi;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Exception;
|
||||
use Throwable;
|
||||
|
||||
class RiwayatController extends Controller
|
||||
{
|
||||
|
|
@ -14,7 +14,7 @@ public function index(Request $request)
|
|||
{
|
||||
try {
|
||||
$perPage = (int) $request->input('per_page', 7);
|
||||
$perPage = in_array($perPage, [5, 7]) ? $perPage : 7;
|
||||
$perPage = in_array($perPage, [5, 7, 10, 25, 50, 100]) ? $perPage : 7;
|
||||
|
||||
$search = trim((string) $request->input('search', ''));
|
||||
|
||||
|
|
@ -49,6 +49,7 @@ public function index(Request $request)
|
|||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Data riwayat berhasil dimuat.',
|
||||
'data' => $riwayat->items(),
|
||||
'current_page' => $riwayat->currentPage(),
|
||||
'last_page' => $riwayat->lastPage(),
|
||||
|
|
@ -57,11 +58,11 @@ public function index(Request $request)
|
|||
'from' => $riwayat->firstItem(),
|
||||
'to' => $riwayat->lastItem(),
|
||||
], 200);
|
||||
|
||||
} catch (Exception $e) {
|
||||
} catch (Throwable $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Error: ' . $e->getMessage()
|
||||
'message' => 'Gagal memuat data riwayat.',
|
||||
'error' => $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
|
@ -69,10 +70,18 @@ public function index(Request $request)
|
|||
public function show($id)
|
||||
{
|
||||
try {
|
||||
$detail = RiwayatRekomendasi::with('user:id,name,email')->findOrFail($id);
|
||||
$detail = RiwayatRekomendasi::with('user:id,name,email')->find($id);
|
||||
|
||||
if (!$detail) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Data detail riwayat tidak ditemukan.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Detail riwayat berhasil ditemukan.',
|
||||
'data' => [
|
||||
'id' => $detail->id,
|
||||
'tanggal' => $detail->created_at ? $detail->created_at->format('d M Y H:i:s') : '-',
|
||||
|
|
@ -85,13 +94,20 @@ public function show($id)
|
|||
'kondisi_ekonomi' => $this->getLabelEkonomi($detail->gaji_ortu),
|
||||
'gaji_ortu' => $detail->gaji_ortu,
|
||||
'nilai_ringkas' => [
|
||||
'Matematika' => $detail->matematika,
|
||||
'Agama' => $detail->agama,
|
||||
'PKN' => $detail->pkn,
|
||||
'B. Indonesia' => $detail->bahasa_indonesia,
|
||||
'B. Inggris' => $detail->bahasa_inggris,
|
||||
'Matematika' => $detail->matematika,
|
||||
'Fisika' => $detail->fisika,
|
||||
'Kimia' => $detail->kimia,
|
||||
'Biologi' => $detail->biologi,
|
||||
'Sejarah' => $detail->sejarah,
|
||||
'Geografi' => $detail->geografi,
|
||||
'Ekonomi' => $detail->ekonomi_mapel,
|
||||
'Sosiologi' => $detail->sosiologi,
|
||||
'Seni Budaya' => $detail->seni_budaya,
|
||||
'PJOK' => $detail->pjok,
|
||||
'Informatika' => $detail->informatika,
|
||||
],
|
||||
'riasec' => [
|
||||
|
|
@ -102,14 +118,41 @@ public function show($id)
|
|||
'Enterprising' => $detail->riasec_e,
|
||||
'Conventional' => $detail->riasec_c,
|
||||
],
|
||||
]
|
||||
],
|
||||
], 200);
|
||||
|
||||
} catch (Exception $e) {
|
||||
} catch (Throwable $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Data detail tidak ditemukan: ' . $e->getMessage()
|
||||
], 404);
|
||||
'message' => 'Terjadi kesalahan saat mengambil detail riwayat.',
|
||||
'error' => $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function destroy($id)
|
||||
{
|
||||
try {
|
||||
$riwayat = RiwayatRekomendasi::find($id);
|
||||
|
||||
if (!$riwayat) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Data riwayat tidak ditemukan.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
$riwayat->delete();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Data riwayat berhasil dihapus.',
|
||||
], 200);
|
||||
} catch (Throwable $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Data riwayat gagal dihapus.',
|
||||
'error' => $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -117,15 +160,13 @@ private function getAkurasiPrediksi($riwayat): ?float
|
|||
{
|
||||
$tableName = (new RiwayatRekomendasi())->getTable();
|
||||
|
||||
if (!Schema::hasColumn($tableName, 'akurasi_prediksi')) {
|
||||
return null;
|
||||
foreach (['akurasi_prediksi', 'probabilitas', 'akurasi'] as $column) {
|
||||
if (Schema::hasColumn($tableName, $column) && $riwayat->{$column} !== null) {
|
||||
return round((float) $riwayat->{$column}, 2);
|
||||
}
|
||||
}
|
||||
|
||||
if ($riwayat->akurasi_prediksi === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return round((float) $riwayat->akurasi_prediksi, 2);
|
||||
return null;
|
||||
}
|
||||
|
||||
private function getRataNilai($riwayat): float
|
||||
|
|
@ -148,7 +189,7 @@ private function getRataNilai($riwayat): float
|
|||
$riwayat->informatika,
|
||||
];
|
||||
|
||||
$nilai = array_filter($nilai, fn ($item) => $item !== null);
|
||||
$nilai = array_filter($nilai, fn ($item) => $item !== null && $item !== '');
|
||||
|
||||
if (count($nilai) === 0) {
|
||||
return 0;
|
||||
|
|
@ -175,6 +216,10 @@ private function getMinatUtama($riwayat): string
|
|||
|
||||
private function getLabelEkonomi($gaji): string
|
||||
{
|
||||
if ($gaji === null || $gaji === '') {
|
||||
return '-';
|
||||
}
|
||||
|
||||
$gaji = (float) $gaji;
|
||||
|
||||
if ($gaji < 2000000) {
|
||||
|
|
|
|||
|
|
@ -4,94 +4,99 @@
|
|||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Siswa;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Throwable;
|
||||
|
||||
class SiswaController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$perPage = (int) $request->input('per_page', 7);
|
||||
$perPage = in_array($perPage, [5, 7]) ? $perPage : 7;
|
||||
try {
|
||||
$perPage = (int) $request->input('per_page', 7);
|
||||
$perPage = in_array($perPage, [5, 7, 10, 25, 50, 100]) ? $perPage : 7;
|
||||
$search = trim((string) $request->input('search', ''));
|
||||
|
||||
$search = trim((string) $request->input('search', ''));
|
||||
$query = Siswa::with('user')->orderBy('id', 'desc');
|
||||
|
||||
$query = Siswa::with(['user', 'jurusan'])->orderBy('id', 'desc');
|
||||
if ($search !== '') {
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('nama_lengkap', 'like', "%{$search}%")
|
||||
->orWhere('nisn', 'like', "%{$search}%")
|
||||
->orWhere('kelas', 'like', "%{$search}%")
|
||||
->orWhereHas('user', function ($user) use ($search) {
|
||||
$user->where('username', 'like', "%{$search}%");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if ($search !== '') {
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('id_siswa', 'like', "%{$search}%")
|
||||
->orWhere('nama_lengkap', 'like', "%{$search}%")
|
||||
->orWhere('nisn', 'like', "%{$search}%")
|
||||
->orWhere('kelas', 'like', "%{$search}%")
|
||||
->orWhere('peminatan', 'like', "%{$search}%")
|
||||
->orWhereHas('user', function ($user) use ($search) {
|
||||
$user->where('name', 'like', "%{$search}%")
|
||||
->orWhere('email', 'like', "%{$search}%");
|
||||
})
|
||||
->orWhereHas('jurusan', function ($jurusan) use ($search) {
|
||||
$jurusan->where('nama_jurusan', 'like', "%{$search}%");
|
||||
});
|
||||
});
|
||||
$siswa = $query->paginate($perPage);
|
||||
|
||||
return response()->json(['success' => true, 'data' => $siswa], 200);
|
||||
} catch (Throwable $e) {
|
||||
return response()->json(['success' => false, 'message' => $e->getMessage()], 500);
|
||||
}
|
||||
|
||||
$siswa = $query->paginate($perPage);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $siswa->items(),
|
||||
'current_page' => $siswa->currentPage(),
|
||||
'last_page' => $siswa->lastPage(),
|
||||
'per_page' => $siswa->perPage(),
|
||||
'total' => $siswa->total(),
|
||||
'from' => $siswa->firstItem(),
|
||||
'to' => $siswa->lastItem(),
|
||||
], 200);
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'user_id' => 'required|exists:users,id|unique:siswas,user_id',
|
||||
'jurusan_id' => 'required|exists:jurusans,id',
|
||||
'nama_lengkap' => 'required|string|max:255',
|
||||
'nisn' => 'required|string|unique:siswas,nisn',
|
||||
'kelas' => 'required|string',
|
||||
'peminatan' => 'required|string',
|
||||
], [
|
||||
'user_id.unique' => 'Gagal! Akun User ID ini sudah terikat dengan data siswa lain.',
|
||||
'nisn.unique' => 'Gagal! NISN ' . $request->nisn . ' sudah terdaftar di sistem.',
|
||||
'nama_lengkap.required' => 'Kolom nama lengkap tidak boleh dikosongkan.',
|
||||
'nisn.required' => 'Kolom NISN wajib diisi.',
|
||||
'user_id.exists' => 'User ID yang Anda masukkan tidak terdaftar di database.',
|
||||
'username' => ['required', Rule::unique(User::class, 'username')],
|
||||
'password' => 'required|min:6',
|
||||
'nama_lengkap' => 'required',
|
||||
'nisn' => ['required', Rule::unique(Siswa::class, 'nisn')],
|
||||
'kelas' => 'required',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Validasi gagal',
|
||||
'errors' => $validator->errors()
|
||||
], 422);
|
||||
return response()->json(['success' => false, 'message' => 'Validasi gagal', 'errors' => $validator->errors()], 422);
|
||||
}
|
||||
|
||||
$siswa = Siswa::create($request->all());
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
$usernameClean = str_replace(' ', '', $request->username);
|
||||
$otomatisEmail = strtolower($usernameClean) . '@gmail.com';
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Siswa ' . $siswa->id_siswa . ' (' . $siswa->nama_lengkap . ') berhasil ditambahkan.',
|
||||
'data' => $siswa
|
||||
], 201);
|
||||
$user = User::create([
|
||||
'name' => $request->nama_lengkap,
|
||||
'username' => $request->username,
|
||||
'email' => $otomatisEmail,
|
||||
'password' => Hash::make($request->password),
|
||||
'role' => 'siswa',
|
||||
]);
|
||||
|
||||
Siswa::create([
|
||||
'user_id' => $user->id,
|
||||
'nama_lengkap' => $request->nama_lengkap,
|
||||
'nisn' => $request->nisn,
|
||||
'kelas' => $request->kelas,
|
||||
]);
|
||||
|
||||
DB::commit();
|
||||
return response()->json(['success' => true, 'message' => 'Data siswa berhasil ditambahkan.'], 201);
|
||||
} catch (\Throwable $e) {
|
||||
DB::rollBack();
|
||||
return response()->json(['success' => false, 'message' => 'Gagal menyimpan data.', 'error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function show($id)
|
||||
{
|
||||
$siswa = Siswa::find($id);
|
||||
try {
|
||||
$siswa = Siswa::with('user')->find($id);
|
||||
|
||||
if (!$siswa) {
|
||||
return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
|
||||
if (!$siswa) {
|
||||
return response()->json(['success' => false, 'message' => 'Data siswa tidak ditemukan.'], 404);
|
||||
}
|
||||
|
||||
return response()->json(['success' => true, 'data' => $siswa], 200);
|
||||
} catch (Throwable $e) {
|
||||
return response()->json(['success' => false, 'message' => 'Gagal memuat rincian.', 'error' => $e->getMessage()], 500);
|
||||
}
|
||||
|
||||
return response()->json(['success' => true, 'data' => $siswa], 200);
|
||||
}
|
||||
|
||||
public function update(Request $request, $id)
|
||||
|
|
@ -99,39 +104,85 @@ public function update(Request $request, $id)
|
|||
$siswa = Siswa::find($id);
|
||||
|
||||
if (!$siswa) {
|
||||
return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
|
||||
return response()->json(['success' => false, 'message' => 'Data siswa tidak ditemukan.'], 404);
|
||||
}
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'nama_lengkap' => 'required|string|max:255',
|
||||
'nisn' => 'required|string|unique:siswas,nisn,' . $id,
|
||||
'kelas' => 'required|string',
|
||||
], [
|
||||
'nisn.unique' => 'Gagal Update! NISN ' . $request->nisn . ' sudah digunakan siswa lain.'
|
||||
'username' => [
|
||||
'nullable',
|
||||
Rule::unique(User::class, 'username')->ignore($siswa->user_id)
|
||||
],
|
||||
'nisn' => [
|
||||
'required',
|
||||
Rule::unique(Siswa::class, 'nisn')->ignore($siswa->id)
|
||||
],
|
||||
'nama_lengkap' => 'required',
|
||||
'kelas' => 'required',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
|
||||
return response()->json(['success' => false, 'message' => 'Validasi gagal', 'errors' => $validator->errors()], 422);
|
||||
}
|
||||
|
||||
$siswa->update($request->all());
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
if ($siswa->user_id) {
|
||||
$user = User::find($siswa->user_id);
|
||||
if ($user) {
|
||||
$user->name = $request->nama_lengkap;
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Data ' . $siswa->id_siswa . ' berhasil diperbarui'
|
||||
], 200);
|
||||
if ($request->filled('username')) {
|
||||
$user->username = $request->username;
|
||||
$usernameClean = str_replace(' ', '', $request->username);
|
||||
$user->email = strtolower($usernameClean) . '@gmail.com';
|
||||
}
|
||||
|
||||
if ($request->filled('password')) {
|
||||
$user->password = Hash::make($request->password);
|
||||
}
|
||||
$user->save();
|
||||
}
|
||||
}
|
||||
|
||||
$siswa->update([
|
||||
'nama_lengkap' => $request->nama_lengkap,
|
||||
'nisn' => $request->nisn,
|
||||
'kelas' => $request->kelas
|
||||
]);
|
||||
|
||||
DB::commit();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Data siswa berhasil diperbarui.',
|
||||
'data' => $siswa->load('user')
|
||||
], 200);
|
||||
} catch (Throwable $e) {
|
||||
DB::rollBack();
|
||||
return response()->json(['success' => false, 'message' => 'Gagal memperbarui data.', 'error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function destroy($id)
|
||||
{
|
||||
$siswa = Siswa::find($id);
|
||||
try {
|
||||
$siswa = Siswa::find($id);
|
||||
|
||||
if (!$siswa) {
|
||||
return response()->json(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
|
||||
if (!$siswa) {
|
||||
return response()->json(['success' => false, 'message' => 'Data tidak ditemukan.'], 404);
|
||||
}
|
||||
|
||||
if ($siswa->user_id) {
|
||||
$userId = $siswa->user_id;
|
||||
$siswa->delete();
|
||||
User::destroy($userId);
|
||||
} else {
|
||||
$siswa->delete();
|
||||
}
|
||||
|
||||
return response()->json(['success' => true, 'message' => 'Data berhasil dihapus.'], 200);
|
||||
} catch (Throwable $e) {
|
||||
return response()->json(['success' => false, 'message' => 'Gagal menghapus.', 'error' => $e->getMessage()], 500);
|
||||
}
|
||||
|
||||
$siswa->delete();
|
||||
|
||||
return response()->json(['success' => true, 'message' => 'Data berhasil dihapus'], 200);
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,6 @@
|
|||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class AuthController extends Controller
|
||||
{
|
||||
|
|
@ -26,10 +25,8 @@ public function login(Request $request)
|
|||
], 401);
|
||||
}
|
||||
|
||||
// Hapus token lama
|
||||
$user->tokens()->delete();
|
||||
|
||||
// Buat token baru
|
||||
$token = $user->createToken('auth_token')->plainTextToken;
|
||||
|
||||
return response()->json([
|
||||
|
|
@ -37,8 +34,9 @@ public function login(Request $request)
|
|||
'message' => 'Login berhasil',
|
||||
'access_token' => $token,
|
||||
'token_type' => 'Bearer',
|
||||
'role' => $user->role, // Penting untuk redirect di frontend
|
||||
'role' => $user->role,
|
||||
'user' => [
|
||||
'id' => $user->id,
|
||||
'name' => $user->name,
|
||||
'username' => $user->username,
|
||||
]
|
||||
|
|
@ -47,7 +45,15 @@ public function login(Request $request)
|
|||
|
||||
public function logout(Request $request)
|
||||
{
|
||||
$request->user()->currentAccessToken()->delete();
|
||||
return response()->json(['message' => 'Berhasil logout']);
|
||||
$token = $request->user()?->currentAccessToken();
|
||||
|
||||
if ($token) {
|
||||
$token->delete();
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Berhasil logout'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -14,8 +14,11 @@ public function index()
|
|||
try {
|
||||
$user = Auth::user();
|
||||
|
||||
if (!$user) {
|
||||
return response()->json(['success' => false, 'message' => 'Unauthorized'], 401);
|
||||
if (! $user) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Unauthorized'
|
||||
], 401);
|
||||
}
|
||||
|
||||
$totalAktivitas = RiwayatRekomendasi::where('user_id', $user->id)->count();
|
||||
|
|
@ -32,7 +35,7 @@ public function index()
|
|||
return [
|
||||
'id' => $item->id,
|
||||
'jurusan' => $item->hasil_rekomendasi_jurusan,
|
||||
'tanggal' => $item->created_at->diffForHumans(),
|
||||
'tanggal' => $item->created_at ? $item->created_at->diffForHumans() : '-',
|
||||
];
|
||||
});
|
||||
|
||||
|
|
@ -41,17 +44,30 @@ public function index()
|
|||
->get()
|
||||
->map(function ($item, $index) {
|
||||
$mapel = [
|
||||
'agama', 'pkn', 'bahasa_indonesia', 'bahasa_inggris', 'matematika',
|
||||
'fisika', 'kimia', 'biologi', 'sejarah', 'geografi',
|
||||
'ekonomi_mapel', 'sosiologi', 'seni_budaya', 'pjok', 'informatika'
|
||||
'agama',
|
||||
'pkn',
|
||||
'bahasa_indonesia',
|
||||
'bahasa_inggris',
|
||||
'matematika',
|
||||
'fisika',
|
||||
'kimia',
|
||||
'biologi',
|
||||
'sejarah',
|
||||
'geografi',
|
||||
'ekonomi_mapel',
|
||||
'sosiologi',
|
||||
'seni_budaya',
|
||||
'pjok',
|
||||
'informatika'
|
||||
];
|
||||
|
||||
$totalNilai = 0;
|
||||
|
||||
foreach ($mapel as $m) {
|
||||
$totalNilai += (float) $item->$m;
|
||||
$totalNilai += (float) ($item->$m ?? 0);
|
||||
}
|
||||
|
||||
$rataRata = $totalNilai / count($mapel);
|
||||
$rataRata = count($mapel) > 0 ? $totalNilai / count($mapel) : 0;
|
||||
|
||||
return [
|
||||
'label' => 'Tes ' . ($index + 1),
|
||||
|
|
@ -63,7 +79,9 @@ public function index()
|
|||
'success' => true,
|
||||
'data' => [
|
||||
'total_aktivitas' => $totalAktivitas,
|
||||
'rekomendasi_terakhir' => $rekomendasiTerakhir ? $rekomendasiTerakhir->hasil_rekomendasi_jurusan : 'Belum Ada Data',
|
||||
'rekomendasi_terakhir' => $rekomendasiTerakhir
|
||||
? $rekomendasiTerakhir->hasil_rekomendasi_jurusan
|
||||
: 'Belum Ada Data',
|
||||
'aktivitas_terbaru' => $aktivitasTerbaru,
|
||||
'tren_nilai' => $trenNilai
|
||||
]
|
||||
|
|
|
|||
|
|
@ -4,18 +4,26 @@
|
|||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\RiwayatRekomendasi;
|
||||
use App\Models\Jurusan;
|
||||
use App\Services\PythonRecommendationService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class RecommendationApiController extends Controller
|
||||
{
|
||||
public function hitungPrediksi(Request $request)
|
||||
public function health(PythonRecommendationService $pythonService)
|
||||
{
|
||||
$result = $pythonService->health();
|
||||
|
||||
return response()->json($result, $result['success'] ? 200 : 500);
|
||||
}
|
||||
|
||||
public function hitungPrediksi(Request $request, PythonRecommendationService $pythonService)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
if (!$user) {
|
||||
if (! $user) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'User belum login.'
|
||||
|
|
@ -49,49 +57,52 @@ public function hitungPrediksi(Request $request)
|
|||
'gaji_ortu' => 'required|numeric|min:0',
|
||||
]);
|
||||
|
||||
$payload = $validated;
|
||||
$payload['ekonomi_orang_tua'] = self::getLabelEkonomiSingkat($validated['gaji_ortu']);
|
||||
$payloadPython = $validated;
|
||||
|
||||
$payloadPython['ekonomi_orang_tua'] = self::getLabelEkonomiSingkat($validated['gaji_ortu']);
|
||||
|
||||
unset($payloadPython['gaji_ortu']);
|
||||
|
||||
try {
|
||||
$response = Http::timeout(15)
|
||||
->acceptJson()
|
||||
->post('http://127.0.0.1:5000/predict', $payload);
|
||||
$pythonResult = $pythonService->predict($payloadPython);
|
||||
|
||||
if (!$response->successful()) {
|
||||
if (! $pythonResult['success']) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'ML Engine Error: ' . ($response->json()['message'] ?? $response->body())
|
||||
'message' => 'ML Engine Error: ' . ($pythonResult['message'] ?? 'Tidak dapat menghubungi Flask API.'),
|
||||
'detail' => $pythonResult,
|
||||
], 500);
|
||||
}
|
||||
|
||||
$responseData = $response->json();
|
||||
$responseData = $pythonResult['data'];
|
||||
|
||||
if (!isset($responseData['success']) || !$responseData['success']) {
|
||||
if (! is_array($responseData)) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Respons Flask API tidak valid.'
|
||||
], 500);
|
||||
}
|
||||
|
||||
if (($responseData['success'] ?? true) === false) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'ML Engine Error: ' . ($responseData['message'] ?? 'Unknown Error')
|
||||
], 500);
|
||||
}
|
||||
|
||||
$teksJurusan = $responseData['prediksi_jurusan'] ?? null;
|
||||
$teksJurusan = $responseData['prediksi_jurusan']
|
||||
?? $responseData['rekomendasi']
|
||||
?? $responseData['jurusan']
|
||||
?? null;
|
||||
|
||||
if (!$teksJurusan) {
|
||||
if (! $teksJurusan) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Prediksi jurusan tidak ditemukan pada respons Flask.'
|
||||
], 500);
|
||||
}
|
||||
|
||||
/*
|
||||
* Ambil nilai probabilitas dari Flask.
|
||||
* Contoh respons Flask yang disarankan:
|
||||
* {
|
||||
* "success": true,
|
||||
* "prediksi_jurusan": "Teknik Informatika",
|
||||
* "probabilitas": 86.45
|
||||
* }
|
||||
*/
|
||||
$akurasiPrediksi = self::normalisasiAkurasi($responseData['probabilitas'] ?? null);
|
||||
$akurasiPrediksi = self::ambilAkurasiDariResponse($responseData, $teksJurusan);
|
||||
|
||||
$dataRiwayat = [
|
||||
'user_id' => $user->id,
|
||||
|
|
@ -123,16 +134,16 @@ public function hitungPrediksi(Request $request)
|
|||
'hasil_rekomendasi_jurusan' => $teksJurusan,
|
||||
];
|
||||
|
||||
/*
|
||||
* Kolom akurasi_prediksi hanya diisi kalau migration baru sudah dijalankan.
|
||||
* Ini mencegah error kalau kolom belum ada.
|
||||
*/
|
||||
$tableName = (new RiwayatRekomendasi())->getTable();
|
||||
|
||||
if (Schema::hasColumn($tableName, 'akurasi_prediksi')) {
|
||||
$dataRiwayat['akurasi_prediksi'] = $akurasiPrediksi;
|
||||
}
|
||||
|
||||
if (Schema::hasColumn($tableName, 'probabilitas')) {
|
||||
$dataRiwayat['probabilitas'] = $akurasiPrediksi;
|
||||
}
|
||||
|
||||
$riwayat = RiwayatRekomendasi::create($dataRiwayat);
|
||||
|
||||
return response()->json([
|
||||
|
|
@ -140,7 +151,9 @@ public function hitungPrediksi(Request $request)
|
|||
'message' => 'Prediksi berhasil dibuat.',
|
||||
'redirect_url' => route('student.result', ['id' => $riwayat->id]),
|
||||
'prediksi_jurusan' => $teksJurusan,
|
||||
'akurasi_prediksi' => $akurasiPrediksi
|
||||
'akurasi_prediksi' => $akurasiPrediksi,
|
||||
'top_predictions' => $responseData['top_predictions'] ?? null,
|
||||
'riwayat_id' => $riwayat->id,
|
||||
]);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
|
|
@ -153,7 +166,16 @@ public function hitungPrediksi(Request $request)
|
|||
|
||||
public function getHasilRekomendasi($id)
|
||||
{
|
||||
$riwayat = RiwayatRekomendasi::findOrFail($id);
|
||||
$user = Auth::user();
|
||||
|
||||
if (! $user) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'User belum login.'
|
||||
], 401);
|
||||
}
|
||||
|
||||
$riwayat = RiwayatRekomendasi::where('user_id', $user->id)->findOrFail($id);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
|
|
@ -166,7 +188,7 @@ public static function formatHasilData(RiwayatRekomendasi $riwayat): array
|
|||
return [
|
||||
'id' => $riwayat->id,
|
||||
'hasil_rekomendasi_jurusan' => $riwayat->hasil_rekomendasi_jurusan,
|
||||
'akurasi_prediksi' => $riwayat->akurasi_prediksi,
|
||||
'akurasi_prediksi' => $riwayat->akurasi_prediksi ?? $riwayat->probabilitas ?? null,
|
||||
'label_ekonomi' => self::getLabelEkonomiDetail($riwayat->gaji_ortu),
|
||||
'minat_utama' => self::getMinatUtamaLabel($riwayat),
|
||||
'prospek_kerja' => self::getProspekKerjaText($riwayat->hasil_rekomendasi_jurusan),
|
||||
|
|
@ -182,6 +204,34 @@ public static function formatHasilData(RiwayatRekomendasi $riwayat): array
|
|||
];
|
||||
}
|
||||
|
||||
private static function ambilAkurasiDariResponse(array $responseData, ?string $jurusan = null): ?float
|
||||
{
|
||||
$nilaiLangsung = $responseData['probabilitas']
|
||||
?? $responseData['confidence']
|
||||
?? $responseData['akurasi_prediksi']
|
||||
?? null;
|
||||
|
||||
if ($nilaiLangsung !== null) {
|
||||
return self::normalisasiAkurasi($nilaiLangsung);
|
||||
}
|
||||
|
||||
$topPredictions = $responseData['top_predictions'] ?? null;
|
||||
|
||||
if (! is_array($topPredictions) || count($topPredictions) === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($jurusan) {
|
||||
foreach ($topPredictions as $item) {
|
||||
if (($item['jurusan'] ?? null) === $jurusan) {
|
||||
return self::normalisasiAkurasi($item['probabilitas'] ?? null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return self::normalisasiAkurasi($topPredictions[0]['probabilitas'] ?? null);
|
||||
}
|
||||
|
||||
private static function normalisasiAkurasi($value): ?float
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
|
|
@ -194,28 +244,16 @@ private static function normalisasiAkurasi($value): ?float
|
|||
$value = trim($value);
|
||||
}
|
||||
|
||||
if (!is_numeric($value)) {
|
||||
if (! is_numeric($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$number = (float) $value;
|
||||
|
||||
/*
|
||||
* Jika Flask mengirim 0.86, ubah menjadi 86.
|
||||
* Jika Flask mengirim 86.45, tetap 86.45.
|
||||
*/
|
||||
if ($number <= 1) {
|
||||
if ($number >= 0 && $number <= 1) {
|
||||
$number = $number * 100;
|
||||
}
|
||||
|
||||
if ($number < 0) {
|
||||
$number = 0;
|
||||
}
|
||||
|
||||
if ($number > 100) {
|
||||
$number = 100;
|
||||
}
|
||||
|
||||
return round($number, 2);
|
||||
}
|
||||
|
||||
|
|
@ -225,7 +263,9 @@ public static function getLabelEkonomiSingkat($gaji): string
|
|||
|
||||
if ($gaji < 2000000) {
|
||||
return 'Kurang Mampu';
|
||||
} elseif ($gaji <= 5000000) {
|
||||
}
|
||||
|
||||
if ($gaji <= 5000000) {
|
||||
return 'Menengah';
|
||||
}
|
||||
|
||||
|
|
@ -237,60 +277,65 @@ public static function getLabelEkonomiDetail($gaji): string
|
|||
$gaji = (float) $gaji;
|
||||
|
||||
if ($gaji < 2000000) {
|
||||
return 'Kurang Mampu (< Rp 2.000.000)';
|
||||
} elseif ($gaji <= 5000000) {
|
||||
return 'Menengah (Rp 2.000.000 - Rp 5.000.000)';
|
||||
return 'Ekonomi Kurang Mampu';
|
||||
}
|
||||
|
||||
return 'Mampu (> Rp 5.000.000)';
|
||||
if ($gaji <= 5000000) {
|
||||
return 'Ekonomi Menengah';
|
||||
}
|
||||
|
||||
return 'Ekonomi Mampu';
|
||||
}
|
||||
|
||||
public static function getMinatUtamaLabel(RiwayatRekomendasi $riwayat): string
|
||||
{
|
||||
$riasecScores = [
|
||||
'r' => (float) $riwayat->riasec_r,
|
||||
'i' => (float) $riwayat->riasec_i,
|
||||
'a' => (float) $riwayat->riasec_a,
|
||||
's' => (float) $riwayat->riasec_s,
|
||||
'e' => (float) $riwayat->riasec_e,
|
||||
'c' => (float) $riwayat->riasec_c,
|
||||
$scores = [
|
||||
'Realistic' => (float) $riwayat->riasec_r,
|
||||
'Investigative' => (float) $riwayat->riasec_i,
|
||||
'Artistic' => (float) $riwayat->riasec_a,
|
||||
'Social' => (float) $riwayat->riasec_s,
|
||||
'Enterprising' => (float) $riwayat->riasec_e,
|
||||
'Conventional' => (float) $riwayat->riasec_c,
|
||||
];
|
||||
|
||||
arsort($riasecScores);
|
||||
$topCode = array_key_first($riasecScores);
|
||||
arsort($scores);
|
||||
|
||||
$labels = [
|
||||
'r' => 'Teknis & Lapangan',
|
||||
'i' => 'Sains, Riset & Analitis',
|
||||
'a' => 'Kreatif & Desain',
|
||||
's' => 'Sosial & Pelayanan',
|
||||
'e' => 'Bisnis & Kepemimpinan',
|
||||
'c' => 'Administratif & Terstruktur',
|
||||
];
|
||||
|
||||
return $labels[$topCode] ?? 'Belum teridentifikasi';
|
||||
return array_key_first($scores) ?? 'Belum diketahui';
|
||||
}
|
||||
|
||||
public static function getProspekKerjaText($jurusan): string
|
||||
{
|
||||
$jurusan = strtolower((string) $jurusan);
|
||||
|
||||
if (str_contains($jurusan, 'ekonomi') || str_contains($jurusan, 'manajemen') || str_contains($jurusan, 'bisnis')) {
|
||||
return 'Lulusan bidang ini dapat berkarier sebagai manajer bisnis, staf keuangan, analis pasar, konsultan bisnis, wirausahawan, maupun pegawai instansi pemerintah dan BUMN pada sektor ekonomi dan administrasi.';
|
||||
}
|
||||
|
||||
if (str_contains($jurusan, 'teknik') || str_contains($jurusan, 'informatika') || str_contains($jurusan, 'komputer')) {
|
||||
return 'Lulusan bidang ini memiliki peluang karier sebagai software developer, analis sistem, teknisi, network engineer, data analyst, maupun profesional teknologi di perusahaan swasta, startup, dan instansi pemerintah.';
|
||||
}
|
||||
|
||||
if (str_contains($jurusan, 'kesehatan') || str_contains($jurusan, 'keperawatan') || str_contains($jurusan, 'farmasi')) {
|
||||
return 'Lulusan bidang ini dapat berkarier di rumah sakit, klinik, laboratorium, industri kesehatan, layanan masyarakat, maupun lembaga penelitian kesehatan.';
|
||||
}
|
||||
|
||||
if (str_contains($jurusan, 'pendidikan') || str_contains($jurusan, 'guru')) {
|
||||
return 'Lulusan bidang ini memiliki prospek sebagai guru, tenaga pendidik, instruktur pelatihan, pengembang kurikulum, maupun konsultan pendidikan.';
|
||||
}
|
||||
|
||||
return 'Prospek karier untuk jurusan ini sangat luas, mulai dari spesialis industri, konsultan ahli, hingga pegawai instansi pemerintah/BUMN di sektor terkait.';
|
||||
public static function getProspekKerjaText(?string $jurusan): string
|
||||
{
|
||||
if (! $jurusan) {
|
||||
return 'Prospek kerja belum tersedia.';
|
||||
}
|
||||
|
||||
// 1. Cari data Jurusan di database berdasarkan nama jurusan hasil prediksi
|
||||
$dataJurusan = \App\Models\Jurusan::where('nama_jurusan', $jurusan)->first();
|
||||
|
||||
// 2. Jika jurusan tidak ditemukan atau tidak memiliki data prospek kerja
|
||||
if (! $dataJurusan || $dataJurusan->prospeks->isEmpty()) {
|
||||
return 'Prospek kerja untuk program studi ' . $jurusan . ' belum diinputkan oleh admin.';
|
||||
}
|
||||
|
||||
// 3. Ambil semua data prospek kerja terkait dan susun menjadi format HTML list yang rapi
|
||||
$htmlOutput = '<ul class="space-y-3">';
|
||||
|
||||
foreach ($dataJurusan->prospeks as $prospek) {
|
||||
$htmlOutput .= '<li class="border-b border-slate-100 pb-2 last:border-0 last:pb-0">';
|
||||
$htmlOutput .= ' <strong class="text-slate-800 block text-base">' . e($prospek->nama_profesi) . '</strong>';
|
||||
|
||||
// Tampilkan range gaji jika admin mengisinya
|
||||
if (!empty($prospek->range_gaji)) {
|
||||
$htmlOutput .= ' <span class="inline-block bg-amber-50 text-amber-700 text-xs px-2 py-0.5 rounded-md font-medium mt-1 mb-1">💰 Estimasi: ' . e($prospek->range_gaji) . '</span>';
|
||||
}
|
||||
|
||||
$htmlOutput .= ' <p class="text-slate-500 text-sm mt-0.5 leading-relaxed">' . e($prospek->deskripsi_pekerjaan) . '</p>';
|
||||
$htmlOutput .= '</li>';
|
||||
}
|
||||
|
||||
$htmlOutput .= '</ul>';
|
||||
|
||||
return $htmlOutput;
|
||||
}
|
||||
}
|
||||
|
|
@ -19,13 +19,18 @@ protected static function boot()
|
|||
{
|
||||
parent::boot();
|
||||
|
||||
// Otomatis membuat id_alumni yang unik dengan aman saat data dibuat
|
||||
static::creating(function ($model) {
|
||||
$latest = static::orderBy('id', 'desc')->first();
|
||||
if (!$latest) {
|
||||
$model->id_alumni = 'AL01';
|
||||
} else {
|
||||
$number = intval(substr($latest->id_alumni, 2)) + 1;
|
||||
$model->id_alumni = 'AL' . str_pad($number, 2, '0', STR_PAD_LEFT);
|
||||
if (empty($model->id_alumni)) {
|
||||
$nextNumber = ((int) static::max('id')) + 1;
|
||||
$candidate = 'ALM' . str_pad($nextNumber, 4, '0', STR_PAD_LEFT);
|
||||
|
||||
while (static::where('id_alumni', $candidate)->exists()) {
|
||||
$nextNumber++;
|
||||
$candidate = 'ALM' . str_pad($nextNumber, 4, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
$model->id_alumni = $candidate;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,22 +3,37 @@
|
|||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Guru extends Model
|
||||
{
|
||||
protected $fillable = ['id_guru', 'nama_guru'];
|
||||
// Memastikan nama tabel sesuai dengan database
|
||||
protected $table = 'gurus';
|
||||
|
||||
|
||||
protected $fillable = ['user_id', 'id_guru', 'nama_guru'];
|
||||
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
|
||||
protected static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::creating(function ($model) {
|
||||
$latest = static::orderBy('id', 'desc')->first();
|
||||
if (!$latest) {
|
||||
$model->id_guru = 'GR01';
|
||||
} else {
|
||||
$number = intval(substr($latest->id_guru, 2)) + 1;
|
||||
$model->id_guru = 'GR' . str_pad($number, 2, '0', STR_PAD_LEFT);
|
||||
if (empty($model->id_guru)) {
|
||||
$nextNumber = ((int) static::max('id')) + 1;
|
||||
$candidate = 'GRU' . str_pad($nextNumber, 4, '0', STR_PAD_LEFT);
|
||||
|
||||
while (static::where('id_guru', $candidate)->exists()) {
|
||||
$nextNumber++;
|
||||
$candidate = 'GRU' . str_pad($nextNumber, 4, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
$model->id_guru = $candidate;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
class Jurusan extends Model
|
||||
{
|
||||
protected $fillable = ['nama_jurusan', 'rumpun', 'deskripsi'];
|
||||
protected $fillable = ['nama_jurusan', 'rumpun',];
|
||||
|
||||
// Relasi: Satu Jurusan memiliki banyak Prospek Kerja
|
||||
public function prospeks()
|
||||
|
|
|
|||
|
|
@ -5,17 +5,15 @@
|
|||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Siswa extends Model
|
||||
|
||||
{
|
||||
protected $table = 'siswas';
|
||||
protected $fillable = [
|
||||
'id_siswa',
|
||||
'user_id',
|
||||
'jurusan_id',
|
||||
'nama_lengkap',
|
||||
'nisn',
|
||||
'kelas',
|
||||
'peminatan',
|
||||
'minat_bakat',
|
||||
'ekonomi'
|
||||
'kelas'
|
||||
];
|
||||
|
||||
protected static function boot()
|
||||
|
|
@ -39,8 +37,4 @@ protected static function boot()
|
|||
public function user() {
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function jurusan() {
|
||||
return $this->belongsTo(Jurusan::class);
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,6 @@
|
|||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||
use HasFactory, Notifiable;
|
||||
use HasApiTokens, HasFactory, Notifiable;
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,12 +6,6 @@
|
|||
|--------------------------------------------------------------------------
|
||||
| Third Party Services
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This file is for storing the credentials for third party services such
|
||||
| as Mailgun, Postmark, AWS and more. This file provides the de facto
|
||||
| location for this type of information, allowing packages to have
|
||||
| a conventional file to locate the various service credentials.
|
||||
|
|
||||
*/
|
||||
|
||||
'postmark' => [
|
||||
|
|
@ -35,4 +29,15 @@
|
|||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Python Flask API
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
'python_api' => [
|
||||
'url' => env('PYTHON_API_URL', 'https://galangadji.pythonanywhere.com'),
|
||||
'timeout' => env('PYTHON_API_TIMEOUT', 30),
|
||||
],
|
||||
|
||||
];
|
||||
|
|
@ -2,6 +2,6 @@ flask
|
|||
flask-cors
|
||||
pandas
|
||||
numpy
|
||||
scikit-learn==1.6.1
|
||||
scikit-learn==1.8.0
|
||||
joblib
|
||||
openpyxl
|
||||
|
|
@ -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());
|
||||
|
|
@ -45,6 +45,7 @@
|
|||
<th class="text-center" width="150">Opsi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody id="alumni-table-body">
|
||||
<tr>
|
||||
<td colspan="6" class="loading-state-modern">
|
||||
|
|
@ -65,13 +66,39 @@
|
|||
</div>
|
||||
|
||||
<script>
|
||||
function getAppBaseUrl() {
|
||||
const origin = window.location.origin;
|
||||
const segments = window.location.pathname.split('/').filter(Boolean);
|
||||
const adminIndex = segments.indexOf('admin');
|
||||
|
||||
if (adminIndex > 0) {
|
||||
return origin + '/' + segments.slice(0, adminIndex).join('/');
|
||||
}
|
||||
|
||||
return origin;
|
||||
}
|
||||
|
||||
const APP_BASE_URL = getAppBaseUrl();
|
||||
const API_ALUMNI_URL = `${APP_BASE_URL}/api/admin/alumni`;
|
||||
const LOGIN_URL = `${APP_BASE_URL}/login`;
|
||||
const ALUMNI_EDIT_BASE_URL = `${APP_BASE_URL}/admin/alumni`;
|
||||
|
||||
let currentPage = 1;
|
||||
let currentSearch = '';
|
||||
let perPage = 7;
|
||||
let searchTimer = null;
|
||||
|
||||
function getToken() {
|
||||
return localStorage.getItem('access_token') ||
|
||||
localStorage.getItem('token') ||
|
||||
sessionStorage.getItem('access_token') ||
|
||||
sessionStorage.getItem('token') ||
|
||||
'';
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
if (value === null || value === undefined || value === '') return '-';
|
||||
|
||||
return String(value)
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
|
|
@ -85,6 +112,19 @@ function getInitial(name) {
|
|||
return cleanName.charAt(0).toUpperCase();
|
||||
}
|
||||
|
||||
async function parseJsonResponse(response) {
|
||||
const text = await response.text();
|
||||
|
||||
try {
|
||||
return text ? JSON.parse(text) : {};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: text || response.statusText || 'Respon server tidak valid.'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function showLoading() {
|
||||
document.getElementById('alumni-table-body').innerHTML = `
|
||||
<tr>
|
||||
|
|
@ -96,11 +136,11 @@ function showLoading() {
|
|||
}
|
||||
|
||||
async function fetchAlumni(page = 1) {
|
||||
const token = localStorage.getItem('access_token');
|
||||
const token = getToken();
|
||||
const tbody = document.getElementById('alumni-table-body');
|
||||
|
||||
if (!token) {
|
||||
window.location.href = '/login';
|
||||
window.location.href = LOGIN_URL;
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -117,14 +157,15 @@ function showLoading() {
|
|||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/admin/alumni?${params.toString()}`, {
|
||||
const response = await fetch(`${API_ALUMNI_URL}?${params.toString()}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
const result = await parseJsonResponse(response);
|
||||
|
||||
if (!response.ok || result.success === false) {
|
||||
throw new Error(result.message || 'Gagal memuat data alumni.');
|
||||
|
|
@ -142,16 +183,21 @@ function showLoading() {
|
|||
</td>
|
||||
</tr>
|
||||
`;
|
||||
|
||||
updatePaginationInfo(null);
|
||||
renderPagination({ current_page: 1, last_page: 1, total: 0 });
|
||||
return;
|
||||
}
|
||||
|
||||
data.forEach((item, index) => {
|
||||
const editUrl = `${ALUMNI_EDIT_BASE_URL}/${encodeURIComponent(item.id)}/edit`;
|
||||
|
||||
tbody.innerHTML += `
|
||||
<tr>
|
||||
<td class="text-center text-muted fw-bold">${(result.from || 1) + index}</td>
|
||||
|
||||
<td class="text-center fw-bold text-muted">${escapeHtml(item.id_alumni)}</td>
|
||||
|
||||
<td>
|
||||
<div class="table-user-cell">
|
||||
<div class="avatar-circle blue">${getInitial(item.nama_alumni)}</div>
|
||||
|
|
@ -161,16 +207,20 @@ function showLoading() {
|
|||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-cell-muted">${escapeHtml(item.perguruan_tinggi)}</td>
|
||||
|
||||
<td class="text-muted fw-semibold">${escapeHtml(item.perguruan_tinggi)}</td>
|
||||
|
||||
<td class="text-center">
|
||||
<span class="badge-soft badge-result">${escapeHtml(item.jurusan_diambil)}</span>
|
||||
<span class="badge-soft badge-kelas">${escapeHtml(item.jurusan_diambil)}</span>
|
||||
</td>
|
||||
|
||||
<td class="text-center">
|
||||
<div class="opsi-wrapper">
|
||||
<a href="/admin/alumni/${item.id}/edit" class="btn-action btn-edit" title="Edit">
|
||||
<a href="${editUrl}" class="btn-action btn-edit" title="Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<button onclick="deleteAlumni(${item.id})" class="btn-action btn-delete" title="Hapus">
|
||||
|
||||
<button type="button" onclick="deleteAlumni(${item.id})" class="btn-action btn-delete" title="Hapus">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -187,7 +237,7 @@ function showLoading() {
|
|||
<tr>
|
||||
<td colspan="6" class="empty-state-modern text-danger">
|
||||
<i class="fas fa-exclamation-circle"></i>
|
||||
Gagal memuat data alumni.
|
||||
${escapeHtml(error.message || 'Gagal memuat data alumni.')}
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
|
|
@ -220,24 +270,32 @@ function renderPagination(data) {
|
|||
btn.className = `page-link-custom ${active ? 'active' : ''}`;
|
||||
btn.innerHTML = label;
|
||||
btn.disabled = disabled;
|
||||
|
||||
btn.onclick = () => {
|
||||
if (!disabled && !active) fetchAlumni(page);
|
||||
};
|
||||
|
||||
return btn;
|
||||
};
|
||||
|
||||
controls.appendChild(createButton('<i class="fas fa-chevron-left"></i>', current - 1, current === 1));
|
||||
|
||||
let pages = [];
|
||||
|
||||
if (last <= 5) {
|
||||
for (let i = 1; i <= last; i++) pages.push(i);
|
||||
} else {
|
||||
pages.push(1);
|
||||
|
||||
if (current > 3) pages.push('...');
|
||||
|
||||
const start = Math.max(2, current - 1);
|
||||
const end = Math.min(last - 1, current + 1);
|
||||
|
||||
for (let i = start; i <= end; i++) pages.push(i);
|
||||
|
||||
if (current < last - 2) pages.push('...');
|
||||
|
||||
pages.push(last);
|
||||
}
|
||||
|
||||
|
|
@ -253,9 +311,9 @@ function renderPagination(data) {
|
|||
}
|
||||
|
||||
async function deleteAlumni(id) {
|
||||
const result = await Swal.fire({
|
||||
title: 'Hapus Data?',
|
||||
text: 'Data alumni akan dihapus secara permanen!',
|
||||
const confirmResult = await Swal.fire({
|
||||
title: 'Hapus data?',
|
||||
text: 'Data alumni akan dihapus permanen!',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#ef4444',
|
||||
|
|
@ -264,12 +322,12 @@ function renderPagination(data) {
|
|||
cancelButtonText: 'Batal'
|
||||
});
|
||||
|
||||
if (!result.isConfirmed) return;
|
||||
if (!confirmResult.isConfirmed) return;
|
||||
|
||||
const token = localStorage.getItem('access_token');
|
||||
const token = getToken();
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/alumni/' + id, {
|
||||
const response = await fetch(`${API_ALUMNI_URL}/${encodeURIComponent(id)}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
|
|
@ -277,14 +335,16 @@ function renderPagination(data) {
|
|||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
Swal.fire('Terhapus!', 'Data alumni berhasil dihapus.', 'success');
|
||||
const result = await parseJsonResponse(response);
|
||||
|
||||
if (response.ok && result.success !== false) {
|
||||
Swal.fire('Berhasil!', result.message || 'Data alumni berhasil dihapus.', 'success');
|
||||
fetchAlumni(currentPage);
|
||||
} else {
|
||||
Swal.fire('Gagal!', 'Data alumni gagal dihapus.', 'error');
|
||||
Swal.fire('Gagal!', result.message || 'Data alumni gagal dihapus.', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
Swal.fire('Error!', 'Koneksi ke server gagal.', 'error');
|
||||
Swal.fire('Error!', error.message || 'Koneksi ke server gagal.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -293,6 +353,7 @@ function renderPagination(data) {
|
|||
|
||||
document.getElementById('searchInput').addEventListener('input', function () {
|
||||
clearTimeout(searchTimer);
|
||||
|
||||
searchTimer = setTimeout(() => {
|
||||
currentSearch = this.value.trim();
|
||||
fetchAlumni(1);
|
||||
|
|
|
|||
|
|
@ -19,18 +19,26 @@
|
|||
<label>Nama Lengkap Alumni</label>
|
||||
<input type="text" id="nama_alumni" class="form-control input-custom" placeholder="Contoh: Ahmad Fauzi" required>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 form-group-custom">
|
||||
<label>Perguruan Tinggi</label>
|
||||
<input type="text" id="perguruan_tinggi" class="form-control input-custom" placeholder="Contoh: Universitas Indonesia" required>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 form-group-custom">
|
||||
<label>Jurusan Kuliah yang Diambil</label>
|
||||
<input type="text" id="jurusan_diambil" class="form-control input-custom" placeholder="Contoh: Teknik Informatika" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 d-flex gap-2 justify-content-end">
|
||||
<button type="submit" id="btnSave" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">Simpan Alumni</button>
|
||||
<a href="{{ route('admin.alumni') }}" class="btn btn-light px-4" style="border-radius: 10px;">Batal</a>
|
||||
<button type="submit" id="btnSave" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">
|
||||
Simpan Alumni
|
||||
</button>
|
||||
|
||||
<a href="{{ route('admin.alumni') }}" class="btn btn-light px-4" style="border-radius: 10px;">
|
||||
Batal
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -39,34 +47,120 @@
|
|||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('formCreateAlumni').addEventListener('submit', async (e) => {
|
||||
function getAppBaseUrl() {
|
||||
const origin = window.location.origin;
|
||||
const segments = window.location.pathname.split('/').filter(Boolean);
|
||||
const adminIndex = segments.indexOf('admin');
|
||||
|
||||
if (adminIndex > 0) {
|
||||
return origin + '/' + segments.slice(0, adminIndex).join('/');
|
||||
}
|
||||
|
||||
return origin;
|
||||
}
|
||||
|
||||
const APP_BASE_URL = getAppBaseUrl();
|
||||
const API_ALUMNI_URL = `${APP_BASE_URL}/api/admin/alumni`;
|
||||
const LOGIN_URL = `${APP_BASE_URL}/login`;
|
||||
const ALUMNI_INDEX_URL = `${APP_BASE_URL}/admin/alumni`;
|
||||
|
||||
function getToken() {
|
||||
return localStorage.getItem('access_token') ||
|
||||
localStorage.getItem('token') ||
|
||||
sessionStorage.getItem('access_token') ||
|
||||
sessionStorage.getItem('token') ||
|
||||
'';
|
||||
}
|
||||
|
||||
function getValue(id) {
|
||||
return document.getElementById(id).value.trim();
|
||||
}
|
||||
|
||||
async function parseJsonResponse(response) {
|
||||
const text = await response.text();
|
||||
|
||||
try {
|
||||
return text ? JSON.parse(text) : {};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: text || response.statusText || 'Respon server tidak valid.'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function formatErrors(errors) {
|
||||
if (!errors) return '';
|
||||
|
||||
return Object.values(errors)
|
||||
.flat()
|
||||
.map(item => String(item))
|
||||
.join('<br>');
|
||||
}
|
||||
|
||||
document.getElementById('formCreateAlumni').addEventListener('submit', async function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const token = getToken();
|
||||
|
||||
if (!token) {
|
||||
Swal.fire('Sesi Berakhir', 'Silakan login ulang.', 'warning')
|
||||
.then(() => window.location.href = LOGIN_URL);
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.getElementById('btnSave');
|
||||
btn.disabled = true;
|
||||
btn.innerText = 'Menyimpan...';
|
||||
|
||||
const payload = {
|
||||
nama_alumni: document.getElementById('nama_alumni').value,
|
||||
perguruan_tinggi: document.getElementById('perguruan_tinggi').value,
|
||||
jurusan_diambil: document.getElementById('jurusan_diambil').value
|
||||
nama_alumni: getValue('nama_alumni'),
|
||||
perguruan_tinggi: getValue('perguruan_tinggi'),
|
||||
jurusan_diambil: getValue('jurusan_diambil')
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/alumni', {
|
||||
const response = await fetch(API_ALUMNI_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('access_token'),
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
Swal.fire({ icon: 'success', title: 'Berhasil!', confirmButtonColor: '#2D6A4F' })
|
||||
.then(() => { window.location.href = "{{ route('admin.alumni') }}"; });
|
||||
const result = await parseJsonResponse(response);
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
Swal.fire('Sesi Berakhir', 'Silakan login ulang.', 'warning')
|
||||
.then(() => window.location.href = LOGIN_URL);
|
||||
return;
|
||||
}
|
||||
} catch (error) { Swal.fire('Error!', 'Gagal menghubungi server.', 'error'); }
|
||||
finally { btn.disabled = false; }
|
||||
|
||||
if (response.ok && result.success !== false) {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Berhasil!',
|
||||
text: result.message || 'Data alumni berhasil ditambahkan.',
|
||||
confirmButtonColor: '#2D7A47'
|
||||
}).then(() => {
|
||||
window.location.href = ALUMNI_INDEX_URL;
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
icon: 'warning',
|
||||
title: 'Gagal!',
|
||||
html: formatErrors(result.errors) || result.message || 'Cek kembali data yang dimasukkan.',
|
||||
confirmButtonColor: '#2D7A47'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
Swal.fire('Error!', error.message || 'Gagal menghubungi server.', 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerText = 'Simpan Alumni';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
|
|
@ -15,23 +15,32 @@
|
|||
<div class="card-body p-5">
|
||||
<form id="formEditAlumni">
|
||||
<input type="hidden" id="alumni_id_db" value="{{ $id }}">
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-md-12 form-group-custom">
|
||||
<label>Nama Lengkap Alumni</label>
|
||||
<input type="text" id="nama_alumni" class="form-control input-custom" required>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 form-group-custom">
|
||||
<label>Perguruan Tinggi</label>
|
||||
<input type="text" id="perguruan_tinggi" class="form-control input-custom" required>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 form-group-custom">
|
||||
<label>Jurusan Kuliah</label>
|
||||
<input type="text" id="jurusan_diambil" class="form-control input-custom" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 d-flex gap-2 justify-content-end">
|
||||
<button type="submit" id="btnUpdate" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">Perbarui Data</button>
|
||||
<a href="{{ route('admin.alumni') }}" class="btn btn-light px-4" style="border-radius: 10px;">Batal</a>
|
||||
<button type="submit" id="btnUpdate" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">
|
||||
Perbarui Data
|
||||
</button>
|
||||
|
||||
<a href="{{ route('admin.alumni') }}" class="btn btn-light px-4" style="border-radius: 10px;">
|
||||
Batal
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -40,51 +49,159 @@
|
|||
</div>
|
||||
|
||||
<script>
|
||||
const dbId = document.getElementById('alumni_id_db').value;
|
||||
function getAppBaseUrl() {
|
||||
const origin = window.location.origin;
|
||||
const segments = window.location.pathname.split('/').filter(Boolean);
|
||||
const adminIndex = segments.indexOf('admin');
|
||||
|
||||
async function loadAlumni() {
|
||||
try {
|
||||
const response = await fetch(`/api/admin/alumni/${dbId}`, {
|
||||
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('access_token') }
|
||||
});
|
||||
const result = await response.json();
|
||||
if (response.ok) {
|
||||
document.getElementById('nama_alumni').value = result.data.nama_alumni;
|
||||
document.getElementById('perguruan_tinggi').value = result.data.perguruan_tinggi;
|
||||
document.getElementById('jurusan_diambil').value = result.data.jurusan_diambil;
|
||||
document.getElementById('display_id_alumni').innerText = `ID Alumni: ${result.data.id_alumni}`;
|
||||
}
|
||||
} catch (error) { console.error(error); }
|
||||
if (adminIndex > 0) {
|
||||
return origin + '/' + segments.slice(0, adminIndex).join('/');
|
||||
}
|
||||
|
||||
return origin;
|
||||
}
|
||||
|
||||
document.getElementById('formEditAlumni').addEventListener('submit', async (e) => {
|
||||
const APP_BASE_URL = getAppBaseUrl();
|
||||
const API_ALUMNI_URL = `${APP_BASE_URL}/api/admin/alumni`;
|
||||
const LOGIN_URL = `${APP_BASE_URL}/login`;
|
||||
const ALUMNI_INDEX_URL = `${APP_BASE_URL}/admin/alumni`;
|
||||
const dbId = document.getElementById('alumni_id_db').value;
|
||||
|
||||
function getToken() {
|
||||
return localStorage.getItem('access_token') ||
|
||||
localStorage.getItem('token') ||
|
||||
sessionStorage.getItem('access_token') ||
|
||||
sessionStorage.getItem('token') ||
|
||||
'';
|
||||
}
|
||||
|
||||
function getValue(id) {
|
||||
return document.getElementById(id).value.trim();
|
||||
}
|
||||
|
||||
async function parseJsonResponse(response) {
|
||||
const text = await response.text();
|
||||
|
||||
try {
|
||||
return text ? JSON.parse(text) : {};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: text || response.statusText || 'Respon server tidak valid.'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function formatErrors(errors) {
|
||||
if (!errors) return '';
|
||||
|
||||
return Object.values(errors)
|
||||
.flat()
|
||||
.map(item => String(item))
|
||||
.join('<br>');
|
||||
}
|
||||
|
||||
async function loadAlumni() {
|
||||
const token = getToken();
|
||||
|
||||
if (!token) {
|
||||
window.location.href = LOGIN_URL;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_ALUMNI_URL}/${encodeURIComponent(dbId)}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const result = await parseJsonResponse(response);
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
Swal.fire('Sesi Berakhir', 'Silakan login ulang.', 'warning')
|
||||
.then(() => window.location.href = LOGIN_URL);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.ok && result.success !== false && result.data) {
|
||||
document.getElementById('nama_alumni').value = result.data.nama_alumni || '';
|
||||
document.getElementById('perguruan_tinggi').value = result.data.perguruan_tinggi || '';
|
||||
document.getElementById('jurusan_diambil').value = result.data.jurusan_diambil || '';
|
||||
document.getElementById('display_id_alumni').innerText = `EDITING: ${result.data.id_alumni || '-'}`;
|
||||
} else {
|
||||
Swal.fire('Gagal!', result.message || 'Data alumni tidak ditemukan.', 'warning');
|
||||
}
|
||||
} catch (error) {
|
||||
Swal.fire('Error!', error.message || 'Gagal mengambil data dari server.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('formEditAlumni').addEventListener('submit', async function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const token = getToken();
|
||||
|
||||
if (!token) {
|
||||
Swal.fire('Sesi Berakhir', 'Silakan login ulang.', 'warning')
|
||||
.then(() => window.location.href = LOGIN_URL);
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.getElementById('btnUpdate');
|
||||
btn.disabled = true;
|
||||
btn.innerText = 'Memperbarui...';
|
||||
|
||||
const payload = {
|
||||
nama_alumni: document.getElementById('nama_alumni').value,
|
||||
perguruan_tinggi: document.getElementById('perguruan_tinggi').value,
|
||||
jurusan_diambil: document.getElementById('jurusan_diambil').value
|
||||
nama_alumni: getValue('nama_alumni'),
|
||||
perguruan_tinggi: getValue('perguruan_tinggi'),
|
||||
jurusan_diambil: getValue('jurusan_diambil')
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/admin/alumni/${dbId}`, {
|
||||
const response = await fetch(`${API_ALUMNI_URL}/${encodeURIComponent(dbId)}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('access_token'),
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
Swal.fire({ icon: 'success', title: 'Berhasil Diperbarui!', confirmButtonColor: '#2D6A4F' })
|
||||
.then(() => { window.location.href = "{{ route('admin.alumni') }}"; });
|
||||
const result = await parseJsonResponse(response);
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
Swal.fire('Sesi Berakhir', 'Silakan login ulang.', 'warning')
|
||||
.then(() => window.location.href = LOGIN_URL);
|
||||
return;
|
||||
}
|
||||
} catch (error) { Swal.fire('Error!', 'Gagal update.', 'error'); }
|
||||
finally { btn.disabled = false; }
|
||||
|
||||
if (response.ok && result.success !== false) {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Berhasil Diperbarui!',
|
||||
text: result.message || 'Data alumni berhasil diperbarui.',
|
||||
confirmButtonColor: '#2D7A47'
|
||||
}).then(() => {
|
||||
window.location.href = ALUMNI_INDEX_URL;
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
icon: 'warning',
|
||||
title: 'Gagal!',
|
||||
html: formatErrors(result.errors) || result.message || 'Cek kembali data yang dimasukkan.',
|
||||
confirmButtonColor: '#2D7A47'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
Swal.fire('Error!', error.message || 'Kesalahan koneksi server.', 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerText = 'Perbarui Data';
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', loadAlumni);
|
||||
|
|
|
|||
|
|
@ -100,11 +100,47 @@
|
|||
</section>
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script>
|
||||
function getAppBaseUrl() {
|
||||
const origin = window.location.origin;
|
||||
const segments = window.location.pathname.split('/').filter(Boolean);
|
||||
const adminIndex = segments.indexOf('admin');
|
||||
|
||||
if (adminIndex > 0) {
|
||||
return origin + '/' + segments.slice(0, adminIndex).join('/');
|
||||
}
|
||||
|
||||
return origin;
|
||||
}
|
||||
|
||||
const APP_BASE_URL = getAppBaseUrl();
|
||||
const API_DASHBOARD_URL = `${APP_BASE_URL}/api/admin/dashboard-stats`;
|
||||
const LOGIN_URL = `${APP_BASE_URL}/login`;
|
||||
|
||||
let distribusiChartInstance = null;
|
||||
|
||||
function getToken() {
|
||||
return localStorage.getItem('access_token') ||
|
||||
localStorage.getItem('token') ||
|
||||
sessionStorage.getItem('access_token') ||
|
||||
sessionStorage.getItem('token') ||
|
||||
'';
|
||||
}
|
||||
|
||||
async function parseJsonResponse(response) {
|
||||
const text = await response.text();
|
||||
|
||||
try {
|
||||
return text ? JSON.parse(text) : {};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: text || response.statusText || 'Respon server tidak valid.'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function setText(id, value) {
|
||||
const element = document.getElementById(id);
|
||||
|
||||
|
|
@ -164,9 +200,7 @@ function renderDistribusiChart(distribusi) {
|
|||
const placeholder = document.getElementById('chart-placeholder');
|
||||
const canvas = document.getElementById('distribusiChart');
|
||||
|
||||
if (!placeholder || !canvas) {
|
||||
return;
|
||||
}
|
||||
if (!placeholder || !canvas) return;
|
||||
|
||||
if (!distribusi || distribusi.length === 0) {
|
||||
placeholder.style.display = 'flex';
|
||||
|
|
@ -270,26 +304,32 @@ function renderDistribusiChart(distribusi) {
|
|||
}
|
||||
|
||||
async function loadDashboardStats() {
|
||||
const token = localStorage.getItem('access_token');
|
||||
const token = getToken();
|
||||
|
||||
if (!token) {
|
||||
window.location.href = '/login';
|
||||
window.location.href = LOGIN_URL;
|
||||
return;
|
||||
}
|
||||
|
||||
updateLastUpdated();
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/dashboard-stats', {
|
||||
const response = await fetch(API_DASHBOARD_URL, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
const result = await parseJsonResponse(response);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
window.location.href = LOGIN_URL;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response.ok || result.success === false) {
|
||||
throw new Error(result.message || 'Gagal memuat data dashboard.');
|
||||
}
|
||||
|
||||
|
|
@ -306,7 +346,7 @@ function renderDistribusiChart(distribusi) {
|
|||
} catch (error) {
|
||||
console.error('Gagal memuat data dashboard:', error);
|
||||
|
||||
setText('akurasi-status', 'Gagal memuat data dashboard.');
|
||||
setText('akurasi-status', error.message || 'Gagal memuat data dashboard.');
|
||||
updateAccuracyRing(0);
|
||||
|
||||
const placeholder = document.getElementById('chart-placeholder');
|
||||
|
|
@ -325,5 +365,4 @@ function renderDistribusiChart(distribusi) {
|
|||
|
||||
document.addEventListener('DOMContentLoaded', loadDashboardStats);
|
||||
</script>
|
||||
@endpush
|
||||
@endsection
|
||||
|
|
@ -43,6 +43,7 @@
|
|||
<th class="text-center" width="150">Opsi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody id="guru-table-body">
|
||||
<tr>
|
||||
<td colspan="4" class="loading-state-modern">
|
||||
|
|
@ -63,13 +64,39 @@
|
|||
</div>
|
||||
|
||||
<script>
|
||||
function getAppBaseUrl() {
|
||||
const origin = window.location.origin;
|
||||
const segments = window.location.pathname.split('/').filter(Boolean);
|
||||
const adminIndex = segments.indexOf('admin');
|
||||
|
||||
if (adminIndex > 0) {
|
||||
return origin + '/' + segments.slice(0, adminIndex).join('/');
|
||||
}
|
||||
|
||||
return origin;
|
||||
}
|
||||
|
||||
const APP_BASE_URL = getAppBaseUrl();
|
||||
const API_GURU_URL = `${APP_BASE_URL}/api/admin/guru`;
|
||||
const LOGIN_URL = `${APP_BASE_URL}/login`;
|
||||
const GURU_EDIT_BASE_URL = `${APP_BASE_URL}/admin/guru`;
|
||||
|
||||
let currentPage = 1;
|
||||
let currentSearch = '';
|
||||
let perPage = 7;
|
||||
let searchTimer = null;
|
||||
|
||||
function getToken() {
|
||||
return localStorage.getItem('access_token') ||
|
||||
localStorage.getItem('token') ||
|
||||
sessionStorage.getItem('access_token') ||
|
||||
sessionStorage.getItem('token') ||
|
||||
'';
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
if (value === null || value === undefined || value === '') return '-';
|
||||
|
||||
return String(value)
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
|
|
@ -83,6 +110,19 @@ function getInitial(name) {
|
|||
return cleanName.charAt(0).toUpperCase();
|
||||
}
|
||||
|
||||
async function parseJsonResponse(response) {
|
||||
const text = await response.text();
|
||||
|
||||
try {
|
||||
return text ? JSON.parse(text) : {};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: text || response.statusText || 'Respon server tidak valid.'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function showLoading() {
|
||||
document.getElementById('guru-table-body').innerHTML = `
|
||||
<tr>
|
||||
|
|
@ -94,11 +134,11 @@ function showLoading() {
|
|||
}
|
||||
|
||||
async function fetchGuru(page = 1) {
|
||||
const token = localStorage.getItem('access_token');
|
||||
const token = getToken();
|
||||
const tbody = document.getElementById('guru-table-body');
|
||||
|
||||
if (!token) {
|
||||
window.location.href = '/login';
|
||||
window.location.href = LOGIN_URL;
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -115,14 +155,15 @@ function showLoading() {
|
|||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/admin/guru?${params.toString()}`, {
|
||||
const response = await fetch(`${API_GURU_URL}?${params.toString()}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
const result = await parseJsonResponse(response);
|
||||
|
||||
if (!response.ok || result.success === false) {
|
||||
throw new Error(result.message || 'Gagal memuat data guru.');
|
||||
|
|
@ -140,31 +181,38 @@ function showLoading() {
|
|||
</td>
|
||||
</tr>
|
||||
`;
|
||||
|
||||
updatePaginationInfo(null);
|
||||
renderPagination({ current_page: 1, last_page: 1, total: 0 });
|
||||
return;
|
||||
}
|
||||
|
||||
data.forEach((item, index) => {
|
||||
const editUrl = `${GURU_EDIT_BASE_URL}/${encodeURIComponent(item.id)}/edit`;
|
||||
|
||||
tbody.innerHTML += `
|
||||
<tr>
|
||||
<td class="text-center text-muted fw-bold">${(result.from || 1) + index}</td>
|
||||
|
||||
<td class="text-center fw-bold text-muted">${escapeHtml(item.id_guru)}</td>
|
||||
|
||||
<td>
|
||||
<div class="table-user-cell">
|
||||
<div class="avatar-circle orange">${getInitial(item.nama_guru)}</div>
|
||||
<div>
|
||||
<div class="fw-bold text-dark">${escapeHtml(item.nama_guru)}</div>
|
||||
<div class="text-cell-muted">Guru Bimbingan Konseling</div>
|
||||
<div class="text-cell-muted">Guru BK</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="text-center">
|
||||
<div class="opsi-wrapper">
|
||||
<a href="/admin/guru/${item.id}/edit" class="btn-action btn-edit" title="Edit">
|
||||
<a href="${editUrl}" class="btn-action btn-edit" title="Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<button onclick="deleteGuru(${item.id})" class="btn-action btn-delete" title="Hapus">
|
||||
|
||||
<button type="button" onclick="deleteGuru(${item.id})" class="btn-action btn-delete" title="Hapus">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -181,7 +229,7 @@ function showLoading() {
|
|||
<tr>
|
||||
<td colspan="4" class="empty-state-modern text-danger">
|
||||
<i class="fas fa-exclamation-circle"></i>
|
||||
Gagal memuat data guru.
|
||||
${escapeHtml(error.message || 'Gagal memuat data guru.')}
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
|
|
@ -214,24 +262,32 @@ function renderPagination(data) {
|
|||
btn.className = `page-link-custom ${active ? 'active' : ''}`;
|
||||
btn.innerHTML = label;
|
||||
btn.disabled = disabled;
|
||||
|
||||
btn.onclick = () => {
|
||||
if (!disabled && !active) fetchGuru(page);
|
||||
};
|
||||
|
||||
return btn;
|
||||
};
|
||||
|
||||
controls.appendChild(createButton('<i class="fas fa-chevron-left"></i>', current - 1, current === 1));
|
||||
|
||||
let pages = [];
|
||||
|
||||
if (last <= 5) {
|
||||
for (let i = 1; i <= last; i++) pages.push(i);
|
||||
} else {
|
||||
pages.push(1);
|
||||
|
||||
if (current > 3) pages.push('...');
|
||||
|
||||
const start = Math.max(2, current - 1);
|
||||
const end = Math.min(last - 1, current + 1);
|
||||
|
||||
for (let i = start; i <= end; i++) pages.push(i);
|
||||
|
||||
if (current < last - 2) pages.push('...');
|
||||
|
||||
pages.push(last);
|
||||
}
|
||||
|
||||
|
|
@ -247,8 +303,8 @@ function renderPagination(data) {
|
|||
}
|
||||
|
||||
async function deleteGuru(id) {
|
||||
const result = await Swal.fire({
|
||||
title: 'Hapus Guru?',
|
||||
const confirmResult = await Swal.fire({
|
||||
title: 'Hapus data?',
|
||||
text: 'Data guru akan dihapus permanen!',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
|
|
@ -258,12 +314,12 @@ function renderPagination(data) {
|
|||
cancelButtonText: 'Batal'
|
||||
});
|
||||
|
||||
if (!result.isConfirmed) return;
|
||||
if (!confirmResult.isConfirmed) return;
|
||||
|
||||
const token = localStorage.getItem('access_token');
|
||||
const token = getToken();
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/guru/' + id, {
|
||||
const response = await fetch(`${API_GURU_URL}/${encodeURIComponent(id)}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
|
|
@ -271,14 +327,16 @@ function renderPagination(data) {
|
|||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
Swal.fire('Terhapus!', 'Data guru berhasil dihapus.', 'success');
|
||||
const result = await parseJsonResponse(response);
|
||||
|
||||
if (response.ok && result.success !== false) {
|
||||
Swal.fire('Berhasil!', result.message || 'Data guru berhasil dihapus.', 'success');
|
||||
fetchGuru(currentPage);
|
||||
} else {
|
||||
Swal.fire('Gagal!', 'Data guru gagal dihapus.', 'error');
|
||||
Swal.fire('Gagal!', result.message || 'Data guru gagal dihapus.', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
Swal.fire('Error!', 'Koneksi ke server gagal.', 'error');
|
||||
Swal.fire('Error!', error.message || 'Koneksi ke server gagal.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -287,6 +345,7 @@ function renderPagination(data) {
|
|||
|
||||
document.getElementById('searchInput').addEventListener('input', function () {
|
||||
clearTimeout(searchTimer);
|
||||
|
||||
searchTimer = setTimeout(() => {
|
||||
currentSearch = this.value.trim();
|
||||
fetchGuru(1);
|
||||
|
|
|
|||
|
|
@ -7,22 +7,51 @@
|
|||
<div class="dashboard-section d-flex justify-content-center">
|
||||
<div style="width: 100%; max-width: 700px;">
|
||||
<div class="mb-4 text-center">
|
||||
<h4 class="fw-bold text-dark">Tambah Guru BK</h4>
|
||||
<p class="text-muted small">Daftarkan akun guru pembimbing baru untuk sistem rekomendasi.</p>
|
||||
<h4 class="fw-bold text-dark">Tambah Guru BK Baru</h4>
|
||||
<p class="text-muted small">Daftarkan akun guru pembimbing baru beserta akun login sistem.</p>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 shadow-sm rounded-4">
|
||||
<div class="card-body p-5">
|
||||
<form id="formCreateGuru">
|
||||
<div class="row g-4">
|
||||
<div class="col-md-12">
|
||||
<h6 class="fw-bold text-success border-bottom pb-2 mb-3"><i class="fas fa-user-tie me-2"></i>Data Profil Guru</h6>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 form-group-custom">
|
||||
<label class="fw-bold small text-muted">Nama Lengkap & Gelar</label>
|
||||
<input type="text" id="nama_guru" class="form-control input-custom" placeholder="Contoh: Budi Santoso, S.Pd" required>
|
||||
<input type="text" id="nama_guru" class="form-control input-custom" placeholder="Contoh: Dra. Endang Sri, M.Pd" required>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 mt-4">
|
||||
<h6 class="fw-bold text-success border-bottom pb-2 mb-3"><i class="fas fa-key me-2"></i>Akun Akses Login Guru</h6>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 form-group-custom">
|
||||
<label class="fw-bold small text-muted">Username Akses</label>
|
||||
<input type="text" id="username" class="form-control input-custom" placeholder="Contoh: endangbk" required>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 form-group-custom">
|
||||
<label class="fw-bold small text-muted">Email Guru</label>
|
||||
<input type="email" id="email" class="form-control input-custom" placeholder="Contoh: endang@sman4jember.sch.id" required>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 form-group-custom">
|
||||
<label class="fw-bold small text-muted">Password Akun</label>
|
||||
<input type="password" id="password" class="form-control input-custom" placeholder="Sandi rahasia minimal 6 karakter" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 d-flex gap-2 justify-content-end">
|
||||
<button type="submit" id="btnSave" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">Daftarkan Guru</button>
|
||||
<a href="{{ route('admin.guru') }}" class="btn btn-light px-4" style="border-radius: 10px;">Batal</a>
|
||||
<button type="submit" id="btnSave" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">
|
||||
Daftarkan Guru & Akun
|
||||
</button>
|
||||
|
||||
<a href="{{ route('admin.guru') }}" class="btn btn-light px-4" style="border-radius: 10px;">
|
||||
Batal
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -31,32 +60,123 @@
|
|||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('formCreateGuru').addEventListener('submit', async (e) => {
|
||||
// Meniru Persis Logika Penentu URL Prospek Kerja
|
||||
function getAppBaseUrl() {
|
||||
const origin = window.location.origin;
|
||||
const segments = window.location.pathname.split('/').filter(Boolean);
|
||||
const adminIndex = segments.indexOf('admin');
|
||||
|
||||
if (adminIndex > 0) {
|
||||
return origin + '/' + segments.slice(0, adminIndex).join('/');
|
||||
}
|
||||
|
||||
return origin;
|
||||
}
|
||||
|
||||
const APP_BASE_URL = getAppBaseUrl();
|
||||
const API_GURU_URL = `${APP_BASE_URL}/api/admin/guru`;
|
||||
const LOGIN_URL = `${APP_BASE_URL}/login`;
|
||||
const GURU_INDEX_URL = `${APP_BASE_URL}/admin/guru`;
|
||||
|
||||
function getToken() {
|
||||
return localStorage.getItem('access_token') ||
|
||||
localStorage.getItem('token') ||
|
||||
sessionStorage.getItem('access_token') ||
|
||||
sessionStorage.getItem('token') ||
|
||||
'';
|
||||
}
|
||||
|
||||
function getValue(id) {
|
||||
return document.getElementById(id).value.trim();
|
||||
}
|
||||
|
||||
async function parseJsonResponse(response) {
|
||||
const text = await response.text();
|
||||
|
||||
try {
|
||||
return text ? JSON.parse(text) : {};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: text || response.statusText || 'Respon server tidak valid.'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function formatErrors(errors) {
|
||||
if (!errors) return '';
|
||||
|
||||
return Object.values(errors)
|
||||
.flat()
|
||||
.map(item => String(item))
|
||||
.join('<br>');
|
||||
}
|
||||
|
||||
// Meniru Persis Logika Submit Event Prospek Kerja
|
||||
document.getElementById('formCreateGuru').addEventListener('submit', async function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const token = getToken();
|
||||
|
||||
if (!token) {
|
||||
Swal.fire('Sesi Berakhir', 'Silakan login ulang.', 'warning')
|
||||
.then(() => window.location.href = LOGIN_URL);
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.getElementById('btnSave');
|
||||
btn.disabled = true;
|
||||
btn.innerText = 'Menyimpan...';
|
||||
|
||||
const payload = {
|
||||
nama_guru: document.getElementById('nama_guru').value
|
||||
nama_guru: getValue('nama_guru'),
|
||||
username: getValue('username'),
|
||||
email: getValue('email'),
|
||||
password: document.getElementById('password').value
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/guru', {
|
||||
const response = await fetch(API_GURU_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('access_token'),
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
Swal.fire({ icon: 'success', title: 'Berhasil!', confirmButtonColor: '#2D6A4F' })
|
||||
.then(() => { window.location.href = "{{ route('admin.guru') }}"; });
|
||||
const result = await parseJsonResponse(response);
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
Swal.fire('Sesi Berakhir', 'Silakan login ulang.', 'warning')
|
||||
.then(() => window.location.href = LOGIN_URL);
|
||||
return;
|
||||
}
|
||||
} catch (error) { Swal.fire('Error!', 'Gagal menghubungi server.', 'error'); }
|
||||
finally { btn.disabled = false; }
|
||||
|
||||
if (response.ok && result.success !== false) {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Berhasil!',
|
||||
text: result.message || 'Data guru dan akun login berhasil disimpan.',
|
||||
confirmButtonColor: '#2D6A4F'
|
||||
}).then(() => {
|
||||
window.location.href = GURU_INDEX_URL;
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
icon: 'warning',
|
||||
title: 'Gagal!',
|
||||
html: formatErrors(result.errors) || result.message || 'Periksa kembali isian Anda.',
|
||||
confirmButtonColor: '#2D6A4F'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
Swal.fire('Error!', error.message || 'Gagal menghubungi server.', 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerText = 'Daftarkan Guru & Akun';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
|
|
@ -15,15 +15,22 @@
|
|||
<div class="card-body p-5">
|
||||
<form id="formEditGuru">
|
||||
<input type="hidden" id="guru_id_db" value="{{ $id }}">
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-md-12 form-group-custom">
|
||||
<label class="fw-bold small text-muted">Nama Lengkap & Gelar</label>
|
||||
<input type="text" id="nama_guru" class="form-control input-custom" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 d-flex gap-2 justify-content-end">
|
||||
<button type="submit" id="btnUpdate" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">Update Profil</button>
|
||||
<a href="{{ route('admin.guru') }}" class="btn btn-light px-4" style="border-radius: 10px;">Batal</a>
|
||||
<button type="submit" id="btnUpdate" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">
|
||||
Update Profil
|
||||
</button>
|
||||
|
||||
<a href="{{ route('admin.guru') }}" class="btn btn-light px-4" style="border-radius: 10px;">
|
||||
Batal
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -32,47 +39,155 @@
|
|||
</div>
|
||||
|
||||
<script>
|
||||
const dbId = document.getElementById('guru_id_db').value;
|
||||
function getAppBaseUrl() {
|
||||
const origin = window.location.origin;
|
||||
const segments = window.location.pathname.split('/').filter(Boolean);
|
||||
const adminIndex = segments.indexOf('admin');
|
||||
|
||||
async function loadGuru() {
|
||||
try {
|
||||
const response = await fetch(`/api/admin/guru/${dbId}`, {
|
||||
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('access_token') }
|
||||
});
|
||||
const result = await response.json();
|
||||
if (response.ok) {
|
||||
document.getElementById('nama_guru').value = result.data.nama_guru;
|
||||
document.getElementById('display_id_guru').innerText = `Ubah profil administrator guru BK (${result.data.id_guru}).`;
|
||||
}
|
||||
} catch (error) { console.error(error); }
|
||||
if (adminIndex > 0) {
|
||||
return origin + '/' + segments.slice(0, adminIndex).join('/');
|
||||
}
|
||||
|
||||
return origin;
|
||||
}
|
||||
|
||||
document.getElementById('formEditGuru').addEventListener('submit', async (e) => {
|
||||
const APP_BASE_URL = getAppBaseUrl();
|
||||
const API_GURU_URL = `${APP_BASE_URL}/api/admin/guru`;
|
||||
const LOGIN_URL = `${APP_BASE_URL}/login`;
|
||||
const GURU_INDEX_URL = `${APP_BASE_URL}/admin/guru`;
|
||||
const dbId = document.getElementById('guru_id_db').value;
|
||||
|
||||
function getToken() {
|
||||
return localStorage.getItem('access_token') ||
|
||||
localStorage.getItem('token') ||
|
||||
sessionStorage.getItem('access_token') ||
|
||||
sessionStorage.getItem('token') ||
|
||||
'';
|
||||
}
|
||||
|
||||
function getValue(id) {
|
||||
return document.getElementById(id).value.trim();
|
||||
}
|
||||
|
||||
async function parseJsonResponse(response) {
|
||||
const text = await response.text();
|
||||
|
||||
try {
|
||||
return text ? JSON.parse(text) : {};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: text || response.statusText || 'Respon server tidak valid.'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function formatErrors(errors) {
|
||||
if (!errors) return '';
|
||||
|
||||
return Object.values(errors)
|
||||
.flat()
|
||||
.map(item => String(item))
|
||||
.join('<br>');
|
||||
}
|
||||
|
||||
async function loadGuru() {
|
||||
const token = getToken();
|
||||
|
||||
if (!token) {
|
||||
window.location.href = LOGIN_URL;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_GURU_URL}/${encodeURIComponent(dbId)}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const result = await parseJsonResponse(response);
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
Swal.fire('Sesi Berakhir', 'Silakan login ulang.', 'warning')
|
||||
.then(() => window.location.href = LOGIN_URL);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.ok && result.success !== false && result.data) {
|
||||
document.getElementById('nama_guru').value = result.data.nama_guru || '';
|
||||
document.getElementById('display_id_guru').innerText = `Ubah profil guru BK (${result.data.id_guru || '-'}).`;
|
||||
} else {
|
||||
Swal.fire('Gagal!', result.message || 'Data guru tidak ditemukan.', 'warning');
|
||||
}
|
||||
} catch (error) {
|
||||
Swal.fire('Error!', error.message || 'Gagal mengambil data dari server.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('formEditGuru').addEventListener('submit', async function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const token = getToken();
|
||||
|
||||
if (!token) {
|
||||
Swal.fire('Sesi Berakhir', 'Silakan login ulang.', 'warning')
|
||||
.then(() => window.location.href = LOGIN_URL);
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.getElementById('btnUpdate');
|
||||
btn.disabled = true;
|
||||
btn.innerText = 'Memperbarui...';
|
||||
|
||||
const payload = {
|
||||
nama_guru: document.getElementById('nama_guru').value
|
||||
nama_guru: getValue('nama_guru')
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/admin/guru/${dbId}`, {
|
||||
const response = await fetch(`${API_GURU_URL}/${encodeURIComponent(dbId)}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('access_token'),
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
Swal.fire({ icon: 'success', title: 'Berhasil Diperbarui!', confirmButtonColor: '#2D6A4F' })
|
||||
.then(() => { window.location.href = "{{ route('admin.guru') }}"; });
|
||||
const result = await parseJsonResponse(response);
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
Swal.fire('Sesi Berakhir', 'Silakan login ulang.', 'warning')
|
||||
.then(() => window.location.href = LOGIN_URL);
|
||||
return;
|
||||
}
|
||||
} catch (error) { Swal.fire('Error!', 'Gagal koneksi.', 'error'); }
|
||||
finally { btn.disabled = false; }
|
||||
|
||||
if (response.ok && result.success !== false) {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Berhasil Diperbarui!',
|
||||
text: result.message || 'Data guru berhasil diperbarui.',
|
||||
confirmButtonColor: '#2D7A47'
|
||||
}).then(() => {
|
||||
window.location.href = GURU_INDEX_URL;
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
icon: 'warning',
|
||||
title: 'Gagal!',
|
||||
html: formatErrors(result.errors) || result.message || 'Cek kembali data yang dimasukkan.',
|
||||
confirmButtonColor: '#2D7A47'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
Swal.fire('Error!', error.message || 'Kesalahan koneksi server.', 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerText = 'Update Profil';
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', loadGuru);
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
<div class="table-toolbar">
|
||||
<div class="search-wrapper-modern">
|
||||
<i class="fas fa-search"></i>
|
||||
<input type="text" id="searchInput" class="search-input-modern" placeholder="Cari nama jurusan, rumpun, atau prospek karir...">
|
||||
<input type="text" id="searchInput" class="search-input-modern" placeholder="Cari nama jurusan, rumpun, atau prospek kerja...">
|
||||
</div>
|
||||
|
||||
<div class="per-page-wrapper">
|
||||
|
|
@ -39,15 +39,15 @@
|
|||
<tr>
|
||||
<th class="text-center" width="70">No</th>
|
||||
<th>Nama Jurusan</th>
|
||||
<th class="text-center">Rumpun</th>
|
||||
<th>Prospek Karir</th>
|
||||
<th>Rumpun</th>
|
||||
<th>Prospek Kerja</th>
|
||||
<th class="text-center" width="150">Opsi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="jurusan-table-body">
|
||||
<tr>
|
||||
<td colspan="5" class="loading-state-modern">
|
||||
<i class="fas fa-spinner fa-spin me-2"></i> Memuat data jurusan...
|
||||
<i class="fas fa-spinner fa-spin me-2"></i> Memuat data...
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
|
@ -64,13 +64,23 @@
|
|||
</div>
|
||||
|
||||
<script>
|
||||
const API_JURUSAN_URL = "{{ url('/api/admin/jurusan') }}";
|
||||
let currentPage = 1;
|
||||
let currentSearch = '';
|
||||
let perPage = 7;
|
||||
let currentSearch = '';
|
||||
let searchTimer = null;
|
||||
|
||||
function getToken() {
|
||||
return localStorage.getItem('access_token') ||
|
||||
localStorage.getItem('token') ||
|
||||
sessionStorage.getItem('access_token') ||
|
||||
sessionStorage.getItem('token') ||
|
||||
'';
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
if (value === null || value === undefined || value === '') return '-';
|
||||
|
||||
return String(value)
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
|
|
@ -79,27 +89,34 @@ function escapeHtml(value) {
|
|||
.replaceAll("'", ''');
|
||||
}
|
||||
|
||||
function showLoading() {
|
||||
document.getElementById('jurusan-table-body').innerHTML = `
|
||||
<tr>
|
||||
<td colspan="5" class="loading-state-modern">
|
||||
<i class="fas fa-spinner fa-spin me-2"></i> Memuat data jurusan...
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
async function parseJsonResponse(response) {
|
||||
const text = await response.text();
|
||||
try {
|
||||
return text ? JSON.parse(text) : {};
|
||||
} catch (e) {
|
||||
return {
|
||||
success: false,
|
||||
message: text || response.statusText || 'Respon server tidak valid.'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchJurusan(page = 1) {
|
||||
const token = localStorage.getItem('access_token');
|
||||
const token = getToken();
|
||||
const tbody = document.getElementById('jurusan-table-body');
|
||||
|
||||
if (!token) {
|
||||
window.location.href = '/login';
|
||||
return;
|
||||
}
|
||||
|
||||
currentPage = page;
|
||||
showLoading();
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="5" class="loading-state-modern">
|
||||
<i class="fas fa-spinner fa-spin me-2"></i> Memuat data...
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
|
||||
const params = new URLSearchParams({
|
||||
page: page,
|
||||
|
|
@ -111,14 +128,15 @@ function showLoading() {
|
|||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/admin/jurusan?${params.toString()}`, {
|
||||
const response = await fetch(`${API_JURUSAN_URL}?${params.toString()}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
const result = await parseJsonResponse(response);
|
||||
|
||||
if (!response.ok || result.success === false) {
|
||||
throw new Error(result.message || 'Gagal memuat data jurusan.');
|
||||
|
|
@ -136,39 +154,53 @@ function showLoading() {
|
|||
</td>
|
||||
</tr>
|
||||
`;
|
||||
|
||||
updatePaginationInfo(null);
|
||||
renderPagination({ current_page: 1, last_page: 1, total: 0 });
|
||||
return;
|
||||
}
|
||||
|
||||
const startNo = result.from || 1;
|
||||
|
||||
data.forEach((item, index) => {
|
||||
const rumpun = item.rumpun || '-';
|
||||
const rumpunClass = rumpun.toLowerCase() === 'saintek' ? 'badge-saintek' : 'badge-soshum';
|
||||
const editUrl = `{{ url('/admin/jurusan') }}/${encodeURIComponent(item.id)}/edit`;
|
||||
|
||||
let prospekText = '-';
|
||||
if (item.prospeks && item.prospeks.length > 0) {
|
||||
prospekText = item.prospeks.map(p => escapeHtml(p.nama_profesi)).join(', ');
|
||||
}
|
||||
|
||||
tbody.innerHTML += `
|
||||
<tr>
|
||||
<td class="text-center text-muted fw-bold">${(result.from || 1) + index}</td>
|
||||
<td class="text-center text-muted fw-bold">${startNo + index}</td>
|
||||
|
||||
<td>
|
||||
<div class="table-user-cell">
|
||||
<div class="avatar-circle purple">
|
||||
<i class="fas fa-graduation-cap"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="fw-bold text-dark text-uppercase">${escapeHtml(item.nama_jurusan)}</div>
|
||||
<div class="fw-bold text-dark">${escapeHtml(item.nama_jurusan)}</div>
|
||||
<div class="text-cell-muted">Data master jurusan</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="badge-soft ${rumpunClass}">${escapeHtml(rumpun)}</span>
|
||||
|
||||
<td>
|
||||
<span class="badge-soft badge-result">${escapeHtml(item.rumpun)}</span>
|
||||
</td>
|
||||
<td class="text-cell-muted">${escapeHtml(item.prospek_karir)}</td>
|
||||
|
||||
<td>
|
||||
<span class="text-muted small fw-medium">${prospekText}</span>
|
||||
</td>
|
||||
|
||||
<td class="text-center">
|
||||
<div class="opsi-wrapper">
|
||||
<a href="/admin/jurusan/${item.id}/edit" class="btn-action btn-edit" title="Edit">
|
||||
<a href="${editUrl}" class="btn-action btn-edit" title="Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<button onclick="deleteJurusan(${item.id})" class="btn-action btn-delete" title="Hapus">
|
||||
|
||||
<button type="button" onclick="deleteJurusan(${item.id}, '${escapeHtml(item.nama_jurusan)}')" class="btn-action btn-delete" title="Hapus">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -185,7 +217,7 @@ function showLoading() {
|
|||
<tr>
|
||||
<td colspan="5" class="empty-state-modern text-danger">
|
||||
<i class="fas fa-exclamation-circle"></i>
|
||||
Gagal memuat data jurusan.
|
||||
${escapeHtml(error.message || 'Gagal memuat data jurusan.')}
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
|
|
@ -218,24 +250,32 @@ function renderPagination(data) {
|
|||
btn.className = `page-link-custom ${active ? 'active' : ''}`;
|
||||
btn.innerHTML = label;
|
||||
btn.disabled = disabled;
|
||||
|
||||
btn.onclick = () => {
|
||||
if (!disabled && !active) fetchJurusan(page);
|
||||
};
|
||||
|
||||
return btn;
|
||||
};
|
||||
|
||||
controls.appendChild(createButton('<i class="fas fa-chevron-left"></i>', current - 1, current === 1));
|
||||
|
||||
let pages = [];
|
||||
|
||||
if (last <= 5) {
|
||||
for (let i = 1; i <= last; i++) pages.push(i);
|
||||
} else {
|
||||
pages.push(1);
|
||||
|
||||
if (current > 3) pages.push('...');
|
||||
|
||||
const start = Math.max(2, current - 1);
|
||||
const end = Math.min(last - 1, current + 1);
|
||||
|
||||
for (let i = start; i <= end; i++) pages.push(i);
|
||||
|
||||
if (current < last - 2) pages.push('...');
|
||||
|
||||
pages.push(last);
|
||||
}
|
||||
|
||||
|
|
@ -250,10 +290,10 @@ function renderPagination(data) {
|
|||
controls.appendChild(createButton('<i class="fas fa-chevron-right"></i>', current + 1, current === last));
|
||||
}
|
||||
|
||||
async function deleteJurusan(id) {
|
||||
const result = await Swal.fire({
|
||||
async function deleteJurusan(id, name) {
|
||||
const confirmResult = await Swal.fire({
|
||||
title: 'Hapus data?',
|
||||
text: 'Data jurusan yang dihapus tidak dapat dikembalikan!',
|
||||
text: `Data jurusan "${name}" beserta prospek kerjanya akan dihapus permanen!`,
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#ef4444',
|
||||
|
|
@ -262,12 +302,12 @@ function renderPagination(data) {
|
|||
cancelButtonText: 'Batal'
|
||||
});
|
||||
|
||||
if (!result.isConfirmed) return;
|
||||
if (!confirmResult.isConfirmed) return;
|
||||
|
||||
const token = localStorage.getItem('access_token');
|
||||
const token = getToken();
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/jurusan/' + id, {
|
||||
const response = await fetch(`${API_JURUSAN_URL}/${encodeURIComponent(id)}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
|
|
@ -275,14 +315,16 @@ function renderPagination(data) {
|
|||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
Swal.fire('Terhapus!', 'Data jurusan berhasil dihapus.', 'success');
|
||||
const resultDelete = await parseJsonResponse(response);
|
||||
|
||||
if (response.ok && resultDelete.success !== false) {
|
||||
Swal.fire('Terhapus!', resultDelete.message || 'Data jurusan berhasil dihapus.', 'success');
|
||||
fetchJurusan(currentPage);
|
||||
} else {
|
||||
Swal.fire('Gagal!', 'Data jurusan gagal dihapus.', 'error');
|
||||
Swal.fire('Gagal!', resultDelete.message || 'Data jurusan gagal dihapus.', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
Swal.fire('Error!', 'Koneksi ke server gagal.', 'error');
|
||||
Swal.fire('Error!', error.message || 'Koneksi ke server gagal.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -291,6 +333,7 @@ function renderPagination(data) {
|
|||
|
||||
document.getElementById('searchInput').addEventListener('input', function () {
|
||||
clearTimeout(searchTimer);
|
||||
|
||||
searchTimer = setTimeout(() => {
|
||||
currentSearch = this.value.trim();
|
||||
fetchJurusan(1);
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
<label class="fw-bold small text-muted">Nama Jurusan</label>
|
||||
<input type="text" id="nama_jurusan" class="form-control input-custom" placeholder="Contoh: Teknik Elektro" required>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 form-group-custom">
|
||||
<label class="fw-bold small text-muted">Rumpun</label>
|
||||
<select id="rumpun" class="form-select input-custom" required>
|
||||
|
|
@ -27,14 +28,16 @@
|
|||
<option value="Soshum">Soshum</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-12 form-group-custom">
|
||||
<label class="fw-bold small text-muted">Prospek Karir</label>
|
||||
<textarea id="prospek_karir" class="form-control input-custom" rows="3" placeholder="Contoh: Engineer, Analyst, Developer"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="mt-5 d-flex gap-2 justify-content-end">
|
||||
<button type="submit" id="btnSave" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">Simpan Data</button>
|
||||
<a href="{{ route('admin.jurusan') }}" class="btn btn-light px-4" style="border-radius: 10px;">Batal</a>
|
||||
<button type="submit" id="btnSave" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">
|
||||
Simpan Data
|
||||
</button>
|
||||
|
||||
<a href="{{ route('admin.jurusan') }}" class="btn btn-light px-4" style="border-radius: 10px;">
|
||||
Batal
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -43,34 +46,119 @@
|
|||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('formCreateJurusan').addEventListener('submit', async (e) => {
|
||||
function getAppBaseUrl() {
|
||||
const origin = window.location.origin;
|
||||
const segments = window.location.pathname.split('/').filter(Boolean);
|
||||
const adminIndex = segments.indexOf('admin');
|
||||
|
||||
if (adminIndex > 0) {
|
||||
return origin + '/' + segments.slice(0, adminIndex).join('/');
|
||||
}
|
||||
|
||||
return origin;
|
||||
}
|
||||
|
||||
const APP_BASE_URL = getAppBaseUrl();
|
||||
const API_JURUSAN_URL = `${APP_BASE_URL}/api/admin/jurusan`;
|
||||
const LOGIN_URL = `${APP_BASE_URL}/login`;
|
||||
const JURUSAN_INDEX_URL = `${APP_BASE_URL}/admin/jurusan`;
|
||||
|
||||
function getToken() {
|
||||
return localStorage.getItem('access_token') ||
|
||||
localStorage.getItem('token') ||
|
||||
sessionStorage.getItem('access_token') ||
|
||||
sessionStorage.getItem('token') ||
|
||||
'';
|
||||
}
|
||||
|
||||
function getValue(id) {
|
||||
return document.getElementById(id).value.trim();
|
||||
}
|
||||
|
||||
async function parseJsonResponse(response) {
|
||||
const text = await response.text();
|
||||
|
||||
try {
|
||||
return text ? JSON.parse(text) : {};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: text || response.statusText || 'Respon server tidak valid.'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function formatErrors(errors) {
|
||||
if (!errors) return '';
|
||||
|
||||
return Object.values(errors)
|
||||
.flat()
|
||||
.map(item => String(item))
|
||||
.join('<br>');
|
||||
}
|
||||
|
||||
document.getElementById('formCreateJurusan').addEventListener('submit', async function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const token = getToken();
|
||||
|
||||
if (!token) {
|
||||
Swal.fire('Sesi Berakhir', 'Silakan login ulang.', 'warning')
|
||||
.then(() => window.location.href = LOGIN_URL);
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.getElementById('btnSave');
|
||||
btn.disabled = true;
|
||||
btn.innerText = 'Menyimpan...';
|
||||
|
||||
const payload = {
|
||||
nama_jurusan: document.getElementById('nama_jurusan').value,
|
||||
rumpun: document.getElementById('rumpun').value,
|
||||
prospek_karir: document.getElementById('prospek_karir').value
|
||||
nama_jurusan: getValue('nama_jurusan'),
|
||||
rumpun: getValue('rumpun'),
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/jurusan', {
|
||||
const response = await fetch(API_JURUSAN_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('access_token'),
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
Swal.fire({ icon: 'success', title: 'Berhasil!', confirmButtonColor: '#2D6A4F' })
|
||||
.then(() => { window.location.href = "{{ route('admin.jurusan') }}"; });
|
||||
const result = await parseJsonResponse(response);
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
Swal.fire('Sesi Berakhir', 'Silakan login ulang.', 'warning')
|
||||
.then(() => window.location.href = LOGIN_URL);
|
||||
return;
|
||||
}
|
||||
} catch (error) { Swal.fire('Error!', 'Gagal menghubungi server.', 'error'); }
|
||||
finally { btn.disabled = false; }
|
||||
|
||||
if (response.ok && result.success !== false) {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Berhasil!',
|
||||
text: result.message || 'Data jurusan berhasil ditambahkan.',
|
||||
confirmButtonColor: '#2D7A47'
|
||||
}).then(() => {
|
||||
window.location.href = JURUSAN_INDEX_URL;
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
icon: 'warning',
|
||||
title: 'Gagal!',
|
||||
html: formatErrors(result.errors) || result.message || 'Cek kembali data yang dimasukkan.',
|
||||
confirmButtonColor: '#2D7A47'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
Swal.fire('Error!', error.message || 'Gagal menghubungi server.', 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerText = 'Simpan Data';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
|
|
@ -15,11 +15,13 @@
|
|||
<div class="card-body p-5">
|
||||
<form id="formEditJurusan">
|
||||
<input type="hidden" id="jurusan_id" value="{{ $id }}">
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-md-12 form-group-custom">
|
||||
<label class="fw-bold small text-muted">Nama Jurusan</label>
|
||||
<input type="text" id="nama_jurusan" class="form-control input-custom" required>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 form-group-custom">
|
||||
<label class="fw-bold small text-muted">Rumpun</label>
|
||||
<select id="rumpun" class="form-select input-custom" required>
|
||||
|
|
@ -27,14 +29,16 @@
|
|||
<option value="Soshum">Soshum</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-12 form-group-custom">
|
||||
<label class="fw-bold small text-muted">Prospek Karir</label>
|
||||
<textarea id="prospek_karir" class="form-control input-custom" rows="3"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="mt-5 d-flex gap-2 justify-content-end">
|
||||
<button type="submit" id="btnUpdate" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">Perbarui Data</button>
|
||||
<a href="{{ route('admin.jurusan') }}" class="btn btn-light px-4" style="border-radius: 10px;">Batal</a>
|
||||
<button type="submit" id="btnUpdate" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">
|
||||
Perbarui Data
|
||||
</button>
|
||||
|
||||
<a href="{{ route('admin.jurusan') }}" class="btn btn-light px-4" style="border-radius: 10px;">
|
||||
Batal
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -43,50 +47,156 @@
|
|||
</div>
|
||||
|
||||
<script>
|
||||
const jurusanId = document.getElementById('jurusan_id').value;
|
||||
function getAppBaseUrl() {
|
||||
const origin = window.location.origin;
|
||||
const segments = window.location.pathname.split('/').filter(Boolean);
|
||||
const adminIndex = segments.indexOf('admin');
|
||||
|
||||
async function loadData() {
|
||||
try {
|
||||
const response = await fetch(`/api/admin/jurusan/${jurusanId}`, {
|
||||
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('access_token') }
|
||||
});
|
||||
const result = await response.json();
|
||||
if (response.ok) {
|
||||
document.getElementById('nama_jurusan').value = result.data.nama_jurusan;
|
||||
document.getElementById('rumpun').value = result.data.rumpun;
|
||||
document.getElementById('prospek_karir').value = result.data.prospek_karir || '';
|
||||
}
|
||||
} catch (error) { console.error(error); }
|
||||
if (adminIndex > 0) {
|
||||
return origin + '/' + segments.slice(0, adminIndex).join('/');
|
||||
}
|
||||
|
||||
return origin;
|
||||
}
|
||||
|
||||
document.getElementById('formEditJurusan').addEventListener('submit', async (e) => {
|
||||
const APP_BASE_URL = getAppBaseUrl();
|
||||
const API_JURUSAN_URL = `${APP_BASE_URL}/api/admin/jurusan`;
|
||||
const LOGIN_URL = `${APP_BASE_URL}/login`;
|
||||
const JURUSAN_INDEX_URL = `${APP_BASE_URL}/admin/jurusan`;
|
||||
const jurusanId = document.getElementById('jurusan_id').value;
|
||||
|
||||
function getToken() {
|
||||
return localStorage.getItem('access_token') ||
|
||||
localStorage.getItem('token') ||
|
||||
sessionStorage.getItem('access_token') ||
|
||||
sessionStorage.getItem('token') ||
|
||||
'';
|
||||
}
|
||||
|
||||
function getValue(id) {
|
||||
return document.getElementById(id).value.trim();
|
||||
}
|
||||
|
||||
async function parseJsonResponse(response) {
|
||||
const text = await response.text();
|
||||
|
||||
try {
|
||||
return text ? JSON.parse(text) : {};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: text || response.statusText || 'Respon server tidak valid.'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function formatErrors(errors) {
|
||||
if (!errors) return '';
|
||||
|
||||
return Object.values(errors)
|
||||
.flat()
|
||||
.map(item => String(item))
|
||||
.join('<br>');
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
const token = getToken();
|
||||
|
||||
if (!token) {
|
||||
window.location.href = LOGIN_URL;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_JURUSAN_URL}/${encodeURIComponent(jurusanId)}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const result = await parseJsonResponse(response);
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
Swal.fire('Sesi Berakhir', 'Silakan login ulang.', 'warning')
|
||||
.then(() => window.location.href = LOGIN_URL);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.ok && result.success !== false && result.data) {
|
||||
document.getElementById('nama_jurusan').value = result.data.nama_jurusan || '';
|
||||
document.getElementById('rumpun').value = result.data.rumpun || '';
|
||||
} else {
|
||||
Swal.fire('Gagal!', result.message || 'Data jurusan tidak ditemukan.', 'warning');
|
||||
}
|
||||
} catch (error) {
|
||||
Swal.fire('Error!', error.message || 'Gagal mengambil data dari server.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('formEditJurusan').addEventListener('submit', async function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const token = getToken();
|
||||
|
||||
if (!token) {
|
||||
Swal.fire('Sesi Berakhir', 'Silakan login ulang.', 'warning')
|
||||
.then(() => window.location.href = LOGIN_URL);
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.getElementById('btnUpdate');
|
||||
btn.disabled = true;
|
||||
btn.innerText = 'Memperbarui...';
|
||||
|
||||
const payload = {
|
||||
nama_jurusan: document.getElementById('nama_jurusan').value,
|
||||
rumpun: document.getElementById('rumpun').value,
|
||||
prospek_karir: document.getElementById('prospek_karir').value
|
||||
nama_jurusan: getValue('nama_jurusan'),
|
||||
rumpun: getValue('rumpun'),
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/admin/jurusan/${jurusanId}`, {
|
||||
const response = await fetch(`${API_JURUSAN_URL}/${encodeURIComponent(jurusanId)}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('access_token'),
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
Swal.fire({ icon: 'success', title: 'Berhasil Diperbarui!', confirmButtonColor: '#2D6A4F' })
|
||||
.then(() => { window.location.href = "{{ route('admin.jurusan') }}"; });
|
||||
const result = await parseJsonResponse(response);
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
Swal.fire('Sesi Berakhir', 'Silakan login ulang.', 'warning')
|
||||
.then(() => window.location.href = LOGIN_URL);
|
||||
return;
|
||||
}
|
||||
} catch (error) { Swal.fire('Error!', 'Gagal koneksi.', 'error'); }
|
||||
finally { btn.disabled = false; }
|
||||
|
||||
if (response.ok && result.success !== false) {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Berhasil Diperbarui!',
|
||||
text: result.message || 'Data jurusan berhasil diperbarui.',
|
||||
confirmButtonColor: '#2D7A47'
|
||||
}).then(() => {
|
||||
window.location.href = JURUSAN_INDEX_URL;
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
icon: 'warning',
|
||||
title: 'Gagal!',
|
||||
html: formatErrors(result.errors) || result.message || 'Cek kembali data yang dimasukkan.',
|
||||
confirmButtonColor: '#2D7A47'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
Swal.fire('Error!', error.message || 'Kesalahan koneksi server.', 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerText = 'Perbarui Data';
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', loadData);
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@
|
|||
<th class="text-center" width="150">Opsi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody id="prospek-table-body">
|
||||
<tr>
|
||||
<td colspan="5" class="loading-state-modern">
|
||||
|
|
@ -58,19 +59,33 @@
|
|||
<div class="text-muted small" id="pagination-info">
|
||||
Menampilkan 0 sampai 0 dari 0 data
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-1 flex-wrap" id="pagination-controls"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API_PROSPEK_URL = @json(url('/api/admin/prospek-kerja'));
|
||||
const LOGIN_URL = @json(url('/login'));
|
||||
const PROSPEK_EDIT_BASE_URL = @json(url('/admin/prospek-kerja'));
|
||||
|
||||
let currentPage = 1;
|
||||
let currentSearch = '';
|
||||
let perPage = 7;
|
||||
let searchTimer = null;
|
||||
|
||||
function getToken() {
|
||||
return localStorage.getItem('access_token') ||
|
||||
localStorage.getItem('token') ||
|
||||
sessionStorage.getItem('access_token') ||
|
||||
sessionStorage.getItem('token') ||
|
||||
'';
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
if (value === null || value === undefined || value === '') return '-';
|
||||
|
||||
return String(value)
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
|
|
@ -79,6 +94,19 @@ function escapeHtml(value) {
|
|||
.replaceAll("'", ''');
|
||||
}
|
||||
|
||||
async function parseJsonResponse(response) {
|
||||
const text = await response.text();
|
||||
|
||||
try {
|
||||
return text ? JSON.parse(text) : {};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: text || response.statusText || 'Respon server tidak valid.'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function showLoading() {
|
||||
document.getElementById('prospek-table-body').innerHTML = `
|
||||
<tr>
|
||||
|
|
@ -90,11 +118,11 @@ function showLoading() {
|
|||
}
|
||||
|
||||
async function fetchProspek(page = 1) {
|
||||
const token = localStorage.getItem('access_token');
|
||||
const token = getToken();
|
||||
const tbody = document.getElementById('prospek-table-body');
|
||||
|
||||
if (!token) {
|
||||
window.location.href = '/login';
|
||||
window.location.href = LOGIN_URL;
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -111,14 +139,27 @@ function showLoading() {
|
|||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/admin/prospek-kerja?${params.toString()}`, {
|
||||
const response = await fetch(`${API_PROSPEK_URL}?${params.toString()}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
const result = await parseJsonResponse(response);
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
Swal.fire('Sesi Berakhir', 'Silakan login ulang.', 'warning')
|
||||
.then(() => {
|
||||
localStorage.removeItem('access_token');
|
||||
localStorage.removeItem('token');
|
||||
sessionStorage.removeItem('access_token');
|
||||
sessionStorage.removeItem('token');
|
||||
window.location.href = LOGIN_URL;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response.ok || result.success === false) {
|
||||
throw new Error(result.message || 'Gagal memuat data prospek kerja.');
|
||||
|
|
@ -136,6 +177,7 @@ function showLoading() {
|
|||
</td>
|
||||
</tr>
|
||||
`;
|
||||
|
||||
updatePaginationInfo(null);
|
||||
renderPagination({ current_page: 1, last_page: 1, total: 0 });
|
||||
return;
|
||||
|
|
@ -143,31 +185,38 @@ function showLoading() {
|
|||
|
||||
data.forEach((item, index) => {
|
||||
const namaJurusan = item.jurusan ? escapeHtml(item.jurusan.nama_jurusan) : 'Jurusan Dihapus';
|
||||
const editUrl = `${PROSPEK_EDIT_BASE_URL}/${encodeURIComponent(item.id)}/edit`;
|
||||
|
||||
tbody.innerHTML += `
|
||||
<tr>
|
||||
<td class="text-center text-muted fw-bold">${(result.from || 1) + index}</td>
|
||||
|
||||
<td>
|
||||
<div class="table-user-cell">
|
||||
<div class="avatar-circle blue">
|
||||
<i class="fas fa-briefcase"></i>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="fw-bold text-dark">${escapeHtml(item.nama_profesi)}</div>
|
||||
<div class="text-cell-muted">Prospek kerja</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<span class="badge-soft badge-result">${namaJurusan}</span>
|
||||
</td>
|
||||
|
||||
<td class="fw-bold text-success">${escapeHtml(item.range_gaji || 'Belum diatur')}</td>
|
||||
|
||||
<td class="text-center">
|
||||
<div class="opsi-wrapper">
|
||||
<a href="/admin/prospek-kerja/${item.id}/edit" class="btn-action btn-edit" title="Edit">
|
||||
<a href="${editUrl}" class="btn-action btn-edit" title="Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<button onclick="deleteProspek(${item.id})" class="btn-action btn-delete" title="Hapus">
|
||||
|
||||
<button type="button" onclick="deleteProspek(${item.id})" class="btn-action btn-delete" title="Hapus">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -184,7 +233,7 @@ function showLoading() {
|
|||
<tr>
|
||||
<td colspan="5" class="empty-state-modern text-danger">
|
||||
<i class="fas fa-exclamation-circle"></i>
|
||||
Gagal memuat data prospek kerja.
|
||||
${escapeHtml(error.message || 'Gagal memuat data prospek kerja.')}
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
|
|
@ -217,24 +266,40 @@ function renderPagination(data) {
|
|||
btn.className = `page-link-custom ${active ? 'active' : ''}`;
|
||||
btn.innerHTML = label;
|
||||
btn.disabled = disabled;
|
||||
|
||||
btn.onclick = () => {
|
||||
if (!disabled && !active) fetchProspek(page);
|
||||
};
|
||||
|
||||
return btn;
|
||||
};
|
||||
|
||||
controls.appendChild(createButton('<i class="fas fa-chevron-left"></i>', current - 1, current === 1));
|
||||
|
||||
let pages = [];
|
||||
|
||||
if (last <= 5) {
|
||||
for (let i = 1; i <= last; i++) pages.push(i);
|
||||
for (let i = 1; i <= last; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
} else {
|
||||
pages.push(1);
|
||||
if (current > 3) pages.push('...');
|
||||
|
||||
if (current > 3) {
|
||||
pages.push('...');
|
||||
}
|
||||
|
||||
const start = Math.max(2, current - 1);
|
||||
const end = Math.min(last - 1, current + 1);
|
||||
for (let i = start; i <= end; i++) pages.push(i);
|
||||
if (current < last - 2) pages.push('...');
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
|
||||
if (current < last - 2) {
|
||||
pages.push('...');
|
||||
}
|
||||
|
||||
pages.push(last);
|
||||
}
|
||||
|
||||
|
|
@ -263,10 +328,10 @@ function renderPagination(data) {
|
|||
|
||||
if (!result.isConfirmed) return;
|
||||
|
||||
const token = localStorage.getItem('access_token');
|
||||
const token = getToken();
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/prospek-kerja/' + id, {
|
||||
const response = await fetch(`${API_PROSPEK_URL}/${encodeURIComponent(id)}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
|
|
@ -274,14 +339,16 @@ function renderPagination(data) {
|
|||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
Swal.fire('Terhapus!', 'Data prospek kerja berhasil dihapus.', 'success');
|
||||
const resultDelete = await parseJsonResponse(response);
|
||||
|
||||
if (response.ok && resultDelete.success !== false) {
|
||||
Swal.fire('Terhapus!', resultDelete.message || 'Data prospek kerja berhasil dihapus.', 'success');
|
||||
fetchProspek(currentPage);
|
||||
} else {
|
||||
Swal.fire('Gagal!', 'Data prospek kerja gagal dihapus.', 'error');
|
||||
Swal.fire('Gagal!', resultDelete.message || 'Data prospek kerja gagal dihapus.', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
Swal.fire('Error!', 'Koneksi ke server gagal.', 'error');
|
||||
Swal.fire('Error!', error.message || 'Koneksi ke server gagal.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -290,6 +357,7 @@ function renderPagination(data) {
|
|||
|
||||
document.getElementById('searchInput').addEventListener('input', function () {
|
||||
clearTimeout(searchTimer);
|
||||
|
||||
searchTimer = setTimeout(() => {
|
||||
currentSearch = this.value.trim();
|
||||
fetchProspek(1);
|
||||
|
|
|
|||
|
|
@ -21,22 +21,31 @@
|
|||
<option value="" disabled selected>Memuat daftar jurusan...</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 form-group-custom">
|
||||
<label class="fw-bold small text-muted">Nama Profesi</label>
|
||||
<input type="text" id="nama_profesi" class="form-control input-custom" placeholder="Contoh: Arsitek Profesional" required>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 form-group-custom">
|
||||
<label class="fw-bold small text-muted">Range Gaji (Opsional)</label>
|
||||
<input type="text" id="range_gaji" class="form-control input-custom" placeholder="Contoh: Rp 6.000.000 - Rp 15.000.000">
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 form-group-custom">
|
||||
<label class="fw-bold small text-muted">Deskripsi Pekerjaan (Opsional)</label>
|
||||
<textarea id="deskripsi_pekerjaan" class="form-control input-custom" rows="3" placeholder="Jelaskan peran pekerjaan ini secara singkat..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 d-flex gap-2 justify-content-end">
|
||||
<button type="submit" id="btnSave" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">Simpan Data</button>
|
||||
<a href="{{ route('admin.prospek-kerja') }}" class="btn btn-light px-4" style="border-radius: 10px;">Batal</a>
|
||||
<button type="submit" id="btnSave" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">
|
||||
Simpan Data
|
||||
</button>
|
||||
|
||||
<a href="{{ route('admin.prospek-kerja') }}" class="btn btn-light px-4" style="border-radius: 10px;">
|
||||
Batal
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -45,54 +54,180 @@
|
|||
</div>
|
||||
|
||||
<script>
|
||||
async function loadJurusan() {
|
||||
try {
|
||||
const response = await fetch('/api/admin/jurusan', {
|
||||
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('access_token') }
|
||||
});
|
||||
const result = await response.json();
|
||||
const select = document.getElementById('jurusan_id');
|
||||
select.innerHTML = '<option value="" disabled selected>Pilih Jurusan Terkait...</option>';
|
||||
function getAppBaseUrl() {
|
||||
const origin = window.location.origin;
|
||||
const segments = window.location.pathname.split('/').filter(Boolean);
|
||||
const adminIndex = segments.indexOf('admin');
|
||||
|
||||
result.data.forEach(item => {
|
||||
select.innerHTML += `<option value="${item.id}">${item.nama_jurusan}</option>`;
|
||||
});
|
||||
if (adminIndex > 0) {
|
||||
return origin + '/' + segments.slice(0, adminIndex).join('/');
|
||||
}
|
||||
|
||||
return origin;
|
||||
}
|
||||
|
||||
const APP_BASE_URL = getAppBaseUrl();
|
||||
const API_PROSPEK_URL = `${APP_BASE_URL}/api/admin/prospek-kerja`;
|
||||
const API_JURUSAN_URL = `${APP_BASE_URL}/api/admin/jurusan`;
|
||||
const LOGIN_URL = `${APP_BASE_URL}/login`;
|
||||
const PROSPEK_INDEX_URL = `${APP_BASE_URL}/admin/prospek-kerja`;
|
||||
|
||||
function getToken() {
|
||||
return localStorage.getItem('access_token') ||
|
||||
localStorage.getItem('token') ||
|
||||
sessionStorage.getItem('access_token') ||
|
||||
sessionStorage.getItem('token') ||
|
||||
'';
|
||||
}
|
||||
|
||||
function getValue(id) {
|
||||
return document.getElementById(id).value.trim();
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
if (value === null || value === undefined || value === '') return '-';
|
||||
|
||||
return String(value)
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''');
|
||||
}
|
||||
|
||||
async function parseJsonResponse(response) {
|
||||
const text = await response.text();
|
||||
|
||||
try {
|
||||
return text ? JSON.parse(text) : {};
|
||||
} catch (error) {
|
||||
document.getElementById('jurusan_id').innerHTML = '<option disabled>Gagal memuat jurusan</option>';
|
||||
return {
|
||||
success: false,
|
||||
message: text || response.statusText || 'Respon server tidak valid.'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('formCreateProspek').addEventListener('submit', async (e) => {
|
||||
function formatErrors(errors) {
|
||||
if (!errors) return '';
|
||||
|
||||
return Object.values(errors)
|
||||
.flat()
|
||||
.map(item => String(item))
|
||||
.join('<br>');
|
||||
}
|
||||
|
||||
async function loadJurusan() {
|
||||
const token = getToken();
|
||||
const select = document.getElementById('jurusan_id');
|
||||
|
||||
if (!token) {
|
||||
window.location.href = LOGIN_URL;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_JURUSAN_URL}?per_page=100`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const result = await parseJsonResponse(response);
|
||||
|
||||
select.innerHTML = '<option value="" disabled selected>Pilih Jurusan Terkait...</option>';
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
window.location.href = LOGIN_URL;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response.ok || result.success === false) {
|
||||
throw new Error(result.message || 'Gagal memuat data jurusan.');
|
||||
}
|
||||
|
||||
const data = result.data || [];
|
||||
|
||||
if (data.length === 0) {
|
||||
select.innerHTML = '<option value="" disabled selected>Belum ada data jurusan</option>';
|
||||
return;
|
||||
}
|
||||
|
||||
data.forEach(item => {
|
||||
select.innerHTML += `<option value="${escapeHtml(item.id)}">${escapeHtml(item.nama_jurusan)}</option>`;
|
||||
});
|
||||
} catch (error) {
|
||||
select.innerHTML = '<option value="" disabled selected>Gagal memuat jurusan</option>';
|
||||
Swal.fire('Error!', error.message || 'Gagal memuat daftar jurusan.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('formCreateProspek').addEventListener('submit', async function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const token = getToken();
|
||||
|
||||
if (!token) {
|
||||
Swal.fire('Sesi Berakhir', 'Silakan login ulang.', 'warning')
|
||||
.then(() => window.location.href = LOGIN_URL);
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.getElementById('btnSave');
|
||||
btn.disabled = true;
|
||||
btn.innerText = 'Menyimpan...';
|
||||
|
||||
const payload = {
|
||||
jurusan_id: document.getElementById('jurusan_id').value,
|
||||
nama_profesi: document.getElementById('nama_profesi').value,
|
||||
range_gaji: document.getElementById('range_gaji').value,
|
||||
deskripsi_pekerjaan: document.getElementById('deskripsi_pekerjaan').value
|
||||
jurusan_id: getValue('jurusan_id'),
|
||||
nama_profesi: getValue('nama_profesi'),
|
||||
range_gaji: getValue('range_gaji'),
|
||||
deskripsi_pekerjaan: getValue('deskripsi_pekerjaan')
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/prospek-kerja', {
|
||||
const response = await fetch(API_PROSPEK_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('access_token'),
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
Swal.fire({ icon: 'success', title: 'Berhasil!', confirmButtonColor: '#2D6A4F' })
|
||||
.then(() => { window.location.href = "{{ route('admin.prospek-kerja') }}"; });
|
||||
} else {
|
||||
Swal.fire('Error!', 'Periksa kembali isian Anda.', 'error');
|
||||
const result = await parseJsonResponse(response);
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
Swal.fire('Sesi Berakhir', 'Silakan login ulang.', 'warning')
|
||||
.then(() => window.location.href = LOGIN_URL);
|
||||
return;
|
||||
}
|
||||
} catch (error) { Swal.fire('Error!', 'Gagal menghubungi server.', 'error'); }
|
||||
finally { btn.disabled = false; }
|
||||
|
||||
if (response.ok && result.success !== false) {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Berhasil!',
|
||||
text: result.message || 'Data prospek kerja berhasil ditambahkan.',
|
||||
confirmButtonColor: '#2D6A4F'
|
||||
}).then(() => {
|
||||
window.location.href = PROSPEK_INDEX_URL;
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
icon: 'warning',
|
||||
title: 'Gagal!',
|
||||
html: formatErrors(result.errors) || result.message || 'Periksa kembali isian Anda.',
|
||||
confirmButtonColor: '#2D6A4F'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
Swal.fire('Error!', error.message || 'Gagal menghubungi server.', 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerText = 'Simpan Data';
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', loadJurusan);
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
<div class="card-body p-5">
|
||||
<form id="formEditProspek">
|
||||
<input type="hidden" id="prospek_id" value="{{ $id }}">
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-md-12 form-group-custom">
|
||||
<label class="fw-bold small text-muted">Kaitkan dengan Jurusan</label>
|
||||
|
|
@ -22,22 +23,31 @@
|
|||
<option value="" disabled>Memuat daftar jurusan...</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 form-group-custom">
|
||||
<label class="fw-bold small text-muted">Nama Profesi</label>
|
||||
<input type="text" id="nama_profesi" class="form-control input-custom" required>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 form-group-custom">
|
||||
<label class="fw-bold small text-muted">Range Gaji</label>
|
||||
<input type="text" id="range_gaji" class="form-control input-custom">
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 form-group-custom">
|
||||
<label class="fw-bold small text-muted">Deskripsi Pekerjaan</label>
|
||||
<textarea id="deskripsi_pekerjaan" class="form-control input-custom" rows="3"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 d-flex gap-2 justify-content-end">
|
||||
<button type="submit" id="btnUpdate" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">Perbarui Data</button>
|
||||
<a href="{{ route('admin.prospek-kerja') }}" class="btn btn-light px-4" style="border-radius: 10px;">Batal</a>
|
||||
<button type="submit" id="btnUpdate" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">
|
||||
Perbarui Data
|
||||
</button>
|
||||
|
||||
<a href="{{ route('admin.prospek-kerja') }}" class="btn btn-light px-4" style="border-radius: 10px;">
|
||||
Batal
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -46,66 +56,212 @@
|
|||
</div>
|
||||
|
||||
<script>
|
||||
const prospekId = document.getElementById('prospek_id').value;
|
||||
function getAppBaseUrl() {
|
||||
const origin = window.location.origin;
|
||||
const segments = window.location.pathname.split('/').filter(Boolean);
|
||||
const adminIndex = segments.indexOf('admin');
|
||||
|
||||
async function loadJurusanAndData() {
|
||||
const token = localStorage.getItem('access_token');
|
||||
if (adminIndex > 0) {
|
||||
return origin + '/' + segments.slice(0, adminIndex).join('/');
|
||||
}
|
||||
|
||||
try {
|
||||
const resJurusan = await fetch('/api/admin/jurusan', {
|
||||
headers: { 'Authorization': 'Bearer ' + token }
|
||||
});
|
||||
const resultJurusan = await resJurusan.json();
|
||||
const select = document.getElementById('jurusan_id');
|
||||
select.innerHTML = '<option value="" disabled>Pilih Jurusan Terkait...</option>';
|
||||
|
||||
resultJurusan.data.forEach(item => {
|
||||
select.innerHTML += `<option value="${item.id}">${item.nama_jurusan}</option>`;
|
||||
});
|
||||
|
||||
const resProspek = await fetch(`/api/admin/prospek-kerja/${prospekId}`, {
|
||||
headers: { 'Authorization': 'Bearer ' + token }
|
||||
});
|
||||
const resultProspek = await resProspek.json();
|
||||
|
||||
if (resProspek.ok) {
|
||||
document.getElementById('jurusan_id').value = resultProspek.data.jurusan_id;
|
||||
document.getElementById('nama_profesi').value = resultProspek.data.nama_profesi;
|
||||
document.getElementById('range_gaji').value = resultProspek.data.range_gaji || '';
|
||||
document.getElementById('deskripsi_pekerjaan').value = resultProspek.data.deskripsi_pekerjaan || '';
|
||||
}
|
||||
} catch (error) { console.error(error); }
|
||||
return origin;
|
||||
}
|
||||
|
||||
document.getElementById('formEditProspek').addEventListener('submit', async (e) => {
|
||||
const APP_BASE_URL = getAppBaseUrl();
|
||||
const API_PROSPEK_URL = `${APP_BASE_URL}/api/admin/prospek-kerja`;
|
||||
const API_JURUSAN_URL = `${APP_BASE_URL}/api/admin/jurusan`;
|
||||
const LOGIN_URL = `${APP_BASE_URL}/login`;
|
||||
const PROSPEK_INDEX_URL = `${APP_BASE_URL}/admin/prospek-kerja`;
|
||||
const prospekId = document.getElementById('prospek_id').value;
|
||||
|
||||
function getToken() {
|
||||
return localStorage.getItem('access_token') ||
|
||||
localStorage.getItem('token') ||
|
||||
sessionStorage.getItem('access_token') ||
|
||||
sessionStorage.getItem('token') ||
|
||||
'';
|
||||
}
|
||||
|
||||
function getValue(id) {
|
||||
return document.getElementById(id).value.trim();
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
if (value === null || value === undefined || value === '') return '-';
|
||||
|
||||
return String(value)
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''');
|
||||
}
|
||||
|
||||
async function parseJsonResponse(response) {
|
||||
const text = await response.text();
|
||||
|
||||
try {
|
||||
return text ? JSON.parse(text) : {};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: text || response.statusText || 'Respon server tidak valid.'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function formatErrors(errors) {
|
||||
if (!errors) return '';
|
||||
|
||||
return Object.values(errors)
|
||||
.flat()
|
||||
.map(item => String(item))
|
||||
.join('<br>');
|
||||
}
|
||||
|
||||
async function loadJurusanOptions(selectedId = '') {
|
||||
const token = getToken();
|
||||
const select = document.getElementById('jurusan_id');
|
||||
|
||||
const response = await fetch(`${API_JURUSAN_URL}?per_page=100`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const result = await parseJsonResponse(response);
|
||||
|
||||
select.innerHTML = '<option value="" disabled>Pilih Jurusan Terkait...</option>';
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
window.location.href = LOGIN_URL;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response.ok || result.success === false) {
|
||||
throw new Error(result.message || 'Gagal memuat data jurusan.');
|
||||
}
|
||||
|
||||
const data = result.data || [];
|
||||
|
||||
if (data.length === 0) {
|
||||
select.innerHTML = '<option value="" disabled>Belum ada data jurusan</option>';
|
||||
return;
|
||||
}
|
||||
|
||||
data.forEach(item => {
|
||||
const selected = String(item.id) === String(selectedId) ? 'selected' : '';
|
||||
select.innerHTML += `<option value="${escapeHtml(item.id)}" ${selected}>${escapeHtml(item.nama_jurusan)}</option>`;
|
||||
});
|
||||
}
|
||||
|
||||
async function loadJurusanAndData() {
|
||||
const token = getToken();
|
||||
|
||||
if (!token) {
|
||||
window.location.href = LOGIN_URL;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const resProspek = await fetch(`${API_PROSPEK_URL}/${encodeURIComponent(prospekId)}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const resultProspek = await parseJsonResponse(resProspek);
|
||||
|
||||
if (resProspek.status === 401 || resProspek.status === 403) {
|
||||
window.location.href = LOGIN_URL;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!resProspek.ok || resultProspek.success === false || !resultProspek.data) {
|
||||
throw new Error(resultProspek.message || 'Data prospek kerja tidak ditemukan.');
|
||||
}
|
||||
|
||||
await loadJurusanOptions(resultProspek.data.jurusan_id);
|
||||
|
||||
document.getElementById('jurusan_id').value = resultProspek.data.jurusan_id || '';
|
||||
document.getElementById('nama_profesi').value = resultProspek.data.nama_profesi || '';
|
||||
document.getElementById('range_gaji').value = resultProspek.data.range_gaji || '';
|
||||
document.getElementById('deskripsi_pekerjaan').value = resultProspek.data.deskripsi_pekerjaan || '';
|
||||
|
||||
} catch (error) {
|
||||
Swal.fire('Error!', error.message || 'Gagal memuat data prospek kerja.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('formEditProspek').addEventListener('submit', async function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const token = getToken();
|
||||
|
||||
if (!token) {
|
||||
Swal.fire('Sesi Berakhir', 'Silakan login ulang.', 'warning')
|
||||
.then(() => window.location.href = LOGIN_URL);
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.getElementById('btnUpdate');
|
||||
btn.disabled = true;
|
||||
btn.innerText = 'Memperbarui...';
|
||||
|
||||
const payload = {
|
||||
jurusan_id: document.getElementById('jurusan_id').value,
|
||||
nama_profesi: document.getElementById('nama_profesi').value,
|
||||
range_gaji: document.getElementById('range_gaji').value,
|
||||
deskripsi_pekerjaan: document.getElementById('deskripsi_pekerjaan').value
|
||||
jurusan_id: getValue('jurusan_id'),
|
||||
nama_profesi: getValue('nama_profesi'),
|
||||
range_gaji: getValue('range_gaji'),
|
||||
deskripsi_pekerjaan: getValue('deskripsi_pekerjaan')
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/admin/prospek-kerja/${prospekId}`, {
|
||||
const response = await fetch(`${API_PROSPEK_URL}/${encodeURIComponent(prospekId)}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('access_token'),
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
Swal.fire({ icon: 'success', title: 'Berhasil Diperbarui!', confirmButtonColor: '#2D6A4F' })
|
||||
.then(() => { window.location.href = "{{ route('admin.prospek-kerja') }}"; });
|
||||
const result = await parseJsonResponse(response);
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
Swal.fire('Sesi Berakhir', 'Silakan login ulang.', 'warning')
|
||||
.then(() => window.location.href = LOGIN_URL);
|
||||
return;
|
||||
}
|
||||
} catch (error) { Swal.fire('Error!', 'Gagal koneksi.', 'error'); }
|
||||
finally { btn.disabled = false; }
|
||||
|
||||
if (response.ok && result.success !== false) {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Berhasil Diperbarui!',
|
||||
text: result.message || 'Data prospek kerja berhasil diperbarui.',
|
||||
confirmButtonColor: '#2D6A4F'
|
||||
}).then(() => {
|
||||
window.location.href = PROSPEK_INDEX_URL;
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
icon: 'warning',
|
||||
title: 'Gagal!',
|
||||
html: formatErrors(result.errors) || result.message || 'Periksa kembali isian Anda.',
|
||||
confirmButtonColor: '#2D6A4F'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
Swal.fire('Error!', error.message || 'Gagal koneksi ke server.', 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerText = 'Perbarui Data';
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', loadJurusanAndData);
|
||||
|
|
|
|||
|
|
@ -94,13 +94,36 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
function getAppBaseUrl() {
|
||||
const origin = window.location.origin;
|
||||
const segments = window.location.pathname.split('/').filter(Boolean);
|
||||
const adminIndex = segments.indexOf('admin');
|
||||
|
||||
if (adminIndex > 0) {
|
||||
return origin + '/' + segments.slice(0, adminIndex).join('/');
|
||||
}
|
||||
|
||||
return origin;
|
||||
}
|
||||
|
||||
const APP_BASE_URL = getAppBaseUrl();
|
||||
const API_RIWAYAT_URL = `${APP_BASE_URL}/api/admin/riwayat`;
|
||||
const LOGIN_URL = `${APP_BASE_URL}/login`;
|
||||
|
||||
let currentPage = 1;
|
||||
let currentSearch = '';
|
||||
let perPage = 7;
|
||||
let searchTimer = null;
|
||||
|
||||
function getToken() {
|
||||
return localStorage.getItem('access_token') ||
|
||||
localStorage.getItem('token') ||
|
||||
sessionStorage.getItem('access_token') ||
|
||||
sessionStorage.getItem('token') ||
|
||||
'';
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
if (value === null || value === undefined || value === '') return '-';
|
||||
|
||||
|
|
@ -112,6 +135,19 @@ function escapeHtml(value) {
|
|||
.replaceAll("'", ''');
|
||||
}
|
||||
|
||||
async function parseJsonResponse(response) {
|
||||
const text = await response.text();
|
||||
|
||||
try {
|
||||
return text ? JSON.parse(text) : {};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: text || response.statusText || 'Respon server tidak valid.'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function formatPercent(value) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return '-';
|
||||
|
|
@ -152,11 +188,11 @@ function showLoading() {
|
|||
}
|
||||
|
||||
async function fetchRiwayat(page = 1) {
|
||||
const token = localStorage.getItem('access_token');
|
||||
const token = getToken();
|
||||
const tbody = document.getElementById('table-riwayat');
|
||||
|
||||
if (!token) {
|
||||
window.location.href = '/login';
|
||||
window.location.href = LOGIN_URL;
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -173,14 +209,20 @@ function showLoading() {
|
|||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/admin/riwayat?${params.toString()}`, {
|
||||
const response = await fetch(`${API_RIWAYAT_URL}?${params.toString()}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
const result = await parseJsonResponse(response);
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
window.location.href = LOGIN_URL;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response.ok || result.success === false) {
|
||||
throw new Error(result.message || 'Gagal memuat data riwayat.');
|
||||
|
|
@ -219,6 +261,7 @@ function showLoading() {
|
|||
<td>
|
||||
<div class="riwayat-user-cell">
|
||||
<div class="riwayat-avatar">${escapeHtml(getInitial(item.nama_siswa))}</div>
|
||||
|
||||
<div>
|
||||
<div class="riwayat-user-name">${escapeHtml(item.nama_siswa)}</div>
|
||||
<div class="riwayat-user-email">${escapeHtml(item.email_siswa)}</div>
|
||||
|
|
@ -245,7 +288,7 @@ function showLoading() {
|
|||
</td>
|
||||
|
||||
<td class="text-center">
|
||||
<button class="btn-detail" onclick="showDetail(${item.id})">
|
||||
<button type="button" class="btn-detail" onclick="showDetail(${item.id})">
|
||||
<i class="fas fa-file-alt me-1"></i> Detail
|
||||
</button>
|
||||
</td>
|
||||
|
|
@ -261,7 +304,7 @@ function showLoading() {
|
|||
<tr>
|
||||
<td colspan="7" class="empty-state-modern text-danger">
|
||||
<i class="fas fa-exclamation-circle"></i>
|
||||
Gagal memuat data riwayat.
|
||||
${escapeHtml(error.message || 'Gagal memuat data riwayat.')}
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
|
|
@ -307,18 +350,26 @@ function renderPagination(data) {
|
|||
let pages = [];
|
||||
|
||||
if (last <= 5) {
|
||||
for (let i = 1; i <= last; i++) pages.push(i);
|
||||
for (let i = 1; i <= last; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
} else {
|
||||
pages.push(1);
|
||||
|
||||
if (current > 3) pages.push('...');
|
||||
if (current > 3) {
|
||||
pages.push('...');
|
||||
}
|
||||
|
||||
const start = Math.max(2, current - 1);
|
||||
const end = Math.min(last - 1, current + 1);
|
||||
|
||||
for (let i = start; i <= end; i++) pages.push(i);
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
|
||||
if (current < last - 2) pages.push('...');
|
||||
if (current < last - 2) {
|
||||
pages.push('...');
|
||||
}
|
||||
|
||||
pages.push(last);
|
||||
}
|
||||
|
|
@ -348,9 +399,14 @@ function renderObjectGrid(data, className) {
|
|||
}
|
||||
|
||||
async function showDetail(id) {
|
||||
const token = localStorage.getItem('access_token');
|
||||
const token = getToken();
|
||||
const modalBody = document.getElementById('modal-body-content');
|
||||
|
||||
if (!token) {
|
||||
window.location.href = LOGIN_URL;
|
||||
return;
|
||||
}
|
||||
|
||||
const detailModal = new bootstrap.Modal(document.getElementById('detailModal'));
|
||||
detailModal.show();
|
||||
|
||||
|
|
@ -362,76 +418,86 @@ function renderObjectGrid(data, className) {
|
|||
`;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/admin/riwayat/${id}`, {
|
||||
const response = await fetch(`${API_RIWAYAT_URL}/${encodeURIComponent(id)}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
const result = await parseJsonResponse(response);
|
||||
|
||||
if (result.success) {
|
||||
const data = result.data;
|
||||
|
||||
modalBody.innerHTML = `
|
||||
<div class="detail-result-card">
|
||||
<small>Hasil Rekomendasi Random Forest</small>
|
||||
<h4>${escapeHtml(data.hasil_rekomendasi)}</h4>
|
||||
</div>
|
||||
|
||||
<div class="detail-grid">
|
||||
<div class="detail-info-card">
|
||||
<span>Nama Siswa</span>
|
||||
<strong>${escapeHtml(data.nama_siswa)}</strong>
|
||||
</div>
|
||||
|
||||
<div class="detail-info-card">
|
||||
<span>Email Siswa</span>
|
||||
<strong>${escapeHtml(data.email_siswa)}</strong>
|
||||
</div>
|
||||
|
||||
<div class="detail-info-card">
|
||||
<span>Waktu Prediksi</span>
|
||||
<strong>${escapeHtml(data.tanggal)}</strong>
|
||||
</div>
|
||||
|
||||
<div class="detail-info-card">
|
||||
<span>Tingkat Keyakinan</span>
|
||||
<strong>${escapeHtml(formatPercent(data.akurasi_prediksi))}</strong>
|
||||
</div>
|
||||
|
||||
<div class="detail-info-card">
|
||||
<span>Minat Utama</span>
|
||||
<strong>${escapeHtml(data.minat_utama)}</strong>
|
||||
</div>
|
||||
|
||||
<div class="detail-info-card">
|
||||
<span>Rata-rata Nilai</span>
|
||||
<strong>${escapeHtml(data.rata_nilai)}</strong>
|
||||
</div>
|
||||
|
||||
<div class="detail-info-card">
|
||||
<span>Kondisi Ekonomi</span>
|
||||
<strong>${escapeHtml(data.kondisi_ekonomi)} · ${escapeHtml(formatRupiah(data.gaji_ortu))}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h6 class="detail-section-title">Nilai Rapor Ringkas</h6>
|
||||
<div class="mini-data-grid">
|
||||
${renderObjectGrid(data.nilai_ringkas, 'mini-data-item')}
|
||||
</div>
|
||||
|
||||
<h6 class="detail-section-title">Skor Minat RIASEC</h6>
|
||||
<div class="mini-data-grid">
|
||||
${renderObjectGrid(data.riasec, 'mini-data-item')}
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
modalBody.innerHTML = `<div class="alert alert-danger m-0">Gagal memuat detail: ${escapeHtml(result.message)}</div>`;
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
window.location.href = LOGIN_URL;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response.ok || result.success === false) {
|
||||
throw new Error(result.message || 'Gagal memuat detail riwayat.');
|
||||
}
|
||||
|
||||
const data = result.data || {};
|
||||
|
||||
modalBody.innerHTML = `
|
||||
<div class="detail-result-card">
|
||||
<small>Hasil Rekomendasi Random Forest</small>
|
||||
<h4>${escapeHtml(data.hasil_rekomendasi)}</h4>
|
||||
</div>
|
||||
|
||||
<div class="detail-grid">
|
||||
<div class="detail-info-card">
|
||||
<span>Nama Siswa</span>
|
||||
<strong>${escapeHtml(data.nama_siswa)}</strong>
|
||||
</div>
|
||||
|
||||
<div class="detail-info-card">
|
||||
<span>Email Siswa</span>
|
||||
<strong>${escapeHtml(data.email_siswa)}</strong>
|
||||
</div>
|
||||
|
||||
<div class="detail-info-card">
|
||||
<span>Waktu Prediksi</span>
|
||||
<strong>${escapeHtml(data.tanggal)}</strong>
|
||||
</div>
|
||||
|
||||
<div class="detail-info-card">
|
||||
<span>Tingkat Keyakinan</span>
|
||||
<strong>${escapeHtml(formatPercent(data.akurasi_prediksi))}</strong>
|
||||
</div>
|
||||
|
||||
<div class="detail-info-card">
|
||||
<span>Minat Utama</span>
|
||||
<strong>${escapeHtml(data.minat_utama)}</strong>
|
||||
</div>
|
||||
|
||||
<div class="detail-info-card">
|
||||
<span>Rata-rata Nilai</span>
|
||||
<strong>${escapeHtml(data.rata_nilai)}</strong>
|
||||
</div>
|
||||
|
||||
<div class="detail-info-card">
|
||||
<span>Kondisi Ekonomi</span>
|
||||
<strong>${escapeHtml(data.kondisi_ekonomi)} · ${escapeHtml(formatRupiah(data.gaji_ortu))}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h6 class="detail-section-title">Nilai Rapor Ringkas</h6>
|
||||
<div class="mini-data-grid">
|
||||
${renderObjectGrid(data.nilai_ringkas, 'mini-data-item')}
|
||||
</div>
|
||||
|
||||
<h6 class="detail-section-title">Skor Minat RIASEC</h6>
|
||||
<div class="mini-data-grid">
|
||||
${renderObjectGrid(data.riasec, 'mini-data-item')}
|
||||
</div>
|
||||
`;
|
||||
} catch (error) {
|
||||
modalBody.innerHTML = `<div class="alert alert-danger m-0">Terjadi kesalahan jaringan saat mengambil detail.</div>`;
|
||||
modalBody.innerHTML = `
|
||||
<div class="alert alert-danger m-0">
|
||||
${escapeHtml(error.message || 'Terjadi kesalahan jaringan saat mengambil detail.')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -453,5 +519,4 @@ function renderObjectGrid(data, className) {
|
|||
});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@endsection
|
||||
|
|
@ -21,271 +21,187 @@
|
|||
<div class="table-toolbar">
|
||||
<div class="search-wrapper-modern">
|
||||
<i class="fas fa-search"></i>
|
||||
<input type="text" id="searchInput" class="search-input-modern" placeholder="Cari nama siswa, NISN, kelas, atau peminatan...">
|
||||
<input type="text" id="searchInput" class="search-input-modern" placeholder="Cari nama siswa, NISN, atau kelas...">
|
||||
</div>
|
||||
|
||||
<div class="per-page-wrapper">
|
||||
<span>Tampilkan</span>
|
||||
<select id="perPageSelect" class="per-page-select">
|
||||
<option value="7" selected>7 data</option>
|
||||
<option value="5">5 data</option>
|
||||
<option value="7" selected>7 data</option>
|
||||
<option value="10">10 data</option>
|
||||
<option value="20">20 data</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-modern mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center" width="70">No</th>
|
||||
<th class="text-center" width="110">ID Siswa</th>
|
||||
<th>Nama Lengkap</th>
|
||||
<th>NISN</th>
|
||||
<th class="text-center">Kelas</th>
|
||||
<th class="text-center" width="150">Opsi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="siswa-table-body">
|
||||
<tr>
|
||||
<td colspan="6" class="loading-state-modern">
|
||||
<i class="fas fa-spinner fa-spin me-2"></i> Memuat data siswa...
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="pagination-container">
|
||||
<div class="text-muted small" id="pagination-info">
|
||||
Menampilkan 0 sampai 0 dari 0 data
|
||||
<div id="tableContent">
|
||||
</div>
|
||||
<div class="d-flex gap-1 flex-wrap" id="pagination-controls"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Konfigurasi API Mandiri
|
||||
const API_SISWA_URL = "https://ta.myhost.id/E31230935/api/admin/siswa";
|
||||
const LOGIN_URL = "https://ta.myhost.id/E31230935/login";
|
||||
|
||||
function getToken() {
|
||||
return localStorage.getItem('access_token');
|
||||
}
|
||||
|
||||
async function parseJsonResponse(response) {
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
Swal.fire('Sesi Berakhir', 'Silakan login ulang.', 'warning')
|
||||
.then(() => window.location.href = LOGIN_URL);
|
||||
return { success: false, message: 'Unauthorized' };
|
||||
}
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
let currentPage = 1;
|
||||
let currentSearch = '';
|
||||
let perPage = 7;
|
||||
let searchTimer = null;
|
||||
|
||||
function escapeHtml(value) {
|
||||
if (value === null || value === undefined || value === '') return '-';
|
||||
return String(value)
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''');
|
||||
}
|
||||
|
||||
function getInitial(name) {
|
||||
const cleanName = name || 'S';
|
||||
return cleanName.charAt(0).toUpperCase();
|
||||
}
|
||||
|
||||
function showLoading() {
|
||||
document.getElementById('siswa-table-body').innerHTML = `
|
||||
<tr>
|
||||
<td colspan="6" class="loading-state-modern">
|
||||
<i class="fas fa-spinner fa-spin me-2"></i> Memuat data siswa...
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
let searchTimer;
|
||||
|
||||
async function fetchSiswa(page = 1) {
|
||||
const token = localStorage.getItem('access_token');
|
||||
const tbody = document.getElementById('siswa-table-body');
|
||||
|
||||
if (!token) {
|
||||
window.location.href = '/login';
|
||||
return;
|
||||
}
|
||||
|
||||
currentPage = page;
|
||||
showLoading();
|
||||
|
||||
const params = new URLSearchParams({
|
||||
page: page,
|
||||
per_page: perPage
|
||||
});
|
||||
|
||||
if (currentSearch !== '') {
|
||||
params.append('search', currentSearch);
|
||||
}
|
||||
const tableContent = document.getElementById('tableContent');
|
||||
tableContent.innerHTML = '<div class="text-center py-5 text-muted"><span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span> Memuat data...</div>';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/admin/siswa?${params.toString()}`, {
|
||||
method: 'GET',
|
||||
const token = getToken();
|
||||
const response = await fetch(`${API_SISWA_URL}?page=${page}&per_page=${perPage}&search=${encodeURIComponent(currentSearch)}`, {
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
const data = await parseJsonResponse(response);
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || result.success === false) {
|
||||
throw new Error(result.message || 'Gagal memuat data siswa.');
|
||||
if (response.ok && data.success !== false) {
|
||||
renderTable(data.data.data, data.data.current_page, data.data.per_page);
|
||||
renderPagination(data.data);
|
||||
} else {
|
||||
tableContent.innerHTML = `<div class="text-center py-5 text-danger"><i class="fas fa-exclamation-triangle fa-3x mb-3"></i><p>Gagal memuat data: ${data.message || 'Error server'}</p></div>`;
|
||||
}
|
||||
|
||||
const data = result.data || [];
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (data.length === 0) {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="6" class="empty-state-modern">
|
||||
<i class="fas fa-folder-open"></i>
|
||||
Tidak ada data siswa yang ditemukan.
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
updatePaginationInfo(null);
|
||||
renderPagination({ current_page: 1, last_page: 1, total: 0 });
|
||||
return;
|
||||
}
|
||||
|
||||
data.forEach((item, index) => {
|
||||
tbody.innerHTML += `
|
||||
<tr>
|
||||
<td class="text-center text-muted fw-bold">${(result.from || 1) + index}</td>
|
||||
<td class="text-center fw-bold text-muted">${escapeHtml(item.id_siswa)}</td>
|
||||
<td>
|
||||
<div class="table-user-cell">
|
||||
<div class="avatar-circle">${getInitial(item.nama_lengkap)}</div>
|
||||
<div>
|
||||
<div class="fw-bold text-dark">${escapeHtml(item.nama_lengkap)}</div>
|
||||
<div class="text-cell-muted">Data siswa aktif</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-muted fw-semibold">${escapeHtml(item.nisn)}</td>
|
||||
<td class="text-center">
|
||||
<span class="badge-soft badge-kelas">${escapeHtml(item.kelas)}</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<div class="opsi-wrapper">
|
||||
<a href="/admin/siswa/${item.id}/edit" class="btn-action btn-edit" title="Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<button onclick="deleteSiswa(${item.id})" class="btn-action btn-delete" title="Hapus">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
|
||||
updatePaginationInfo(result);
|
||||
renderPagination(result);
|
||||
|
||||
} catch (error) {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="6" class="empty-state-modern text-danger">
|
||||
<i class="fas fa-exclamation-circle"></i>
|
||||
Gagal memuat data siswa.
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
tableContent.innerHTML = `<div class="text-center py-5 text-danger"><i class="fas fa-wifi fa-3x mb-3"></i><p>Gagal menghubungi server. Periksa koneksi internet Anda.</p></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function updatePaginationInfo(data) {
|
||||
const info = document.getElementById('pagination-info');
|
||||
function renderTable(siswaData, currentPage, perPage) {
|
||||
const tableContent = document.getElementById('tableContent');
|
||||
|
||||
if (!data || !data.total) {
|
||||
info.innerText = 'Menampilkan 0 sampai 0 dari 0 data';
|
||||
if (siswaData.length === 0) {
|
||||
tableContent.innerHTML = '<div class="text-center py-5 text-muted"><i class="fas fa-folder-open fa-3x mb-3"></i><p>Tidak ada data siswa yang ditemukan.</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
info.innerText = `Menampilkan ${data.from || 0} sampai ${data.to || 0} dari ${data.total || 0} data`;
|
||||
}
|
||||
let html = `
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle table-modern">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="5%" class="text-center">No</th>
|
||||
<th width="25%">Nama Lengkap</th>
|
||||
<th width="20%">NISN</th>
|
||||
<th width="15%" class="text-center">Kelas</th>
|
||||
<th width="20%">Username</th>
|
||||
<th width="15%" class="text-center">Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
`;
|
||||
|
||||
function renderPagination(data) {
|
||||
const controls = document.getElementById('pagination-controls');
|
||||
controls.innerHTML = '';
|
||||
siswaData.forEach((siswa, index) => {
|
||||
const no = (currentPage - 1) * perPage + (index + 1);
|
||||
const editUrl = `{{ url('/admin/siswa') }}/${siswa.id}/edit`;
|
||||
const username = siswa.user ? siswa.user.username : '<span class="text-danger small">Tidak ada akun</span>';
|
||||
|
||||
const current = Number(data.current_page || 1);
|
||||
const last = Number(data.last_page || 1);
|
||||
const total = Number(data.total || 0);
|
||||
|
||||
if (total === 0) return;
|
||||
|
||||
const createButton = (label, page, disabled = false, active = false) => {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = `page-link-custom ${active ? 'active' : ''}`;
|
||||
btn.innerHTML = label;
|
||||
btn.disabled = disabled;
|
||||
btn.onclick = () => {
|
||||
if (!disabled && !active) fetchSiswa(page);
|
||||
};
|
||||
return btn;
|
||||
};
|
||||
|
||||
controls.appendChild(createButton('<i class="fas fa-chevron-left"></i>', current - 1, current === 1));
|
||||
|
||||
let pages = [];
|
||||
if (last <= 5) {
|
||||
for (let i = 1; i <= last; i++) pages.push(i);
|
||||
} else {
|
||||
pages.push(1);
|
||||
if (current > 3) pages.push('...');
|
||||
const start = Math.max(2, current - 1);
|
||||
const end = Math.min(last - 1, current + 1);
|
||||
for (let i = start; i <= end; i++) pages.push(i);
|
||||
if (current < last - 2) pages.push('...');
|
||||
pages.push(last);
|
||||
}
|
||||
|
||||
pages.forEach(page => {
|
||||
if (page === '...') {
|
||||
controls.appendChild(createButton('...', current, true));
|
||||
} else {
|
||||
controls.appendChild(createButton(page, page, false, page === current));
|
||||
}
|
||||
html += `
|
||||
<tr>
|
||||
<td class="text-center">${no}</td>
|
||||
<td class="fw-bold">${siswa.nama_lengkap || '-'}</td>
|
||||
<td><span class="badge bg-light text-dark border">${siswa.nisn || '-'}</span></td>
|
||||
<td class="text-center"><span class="badge" style="background-color: #2D7A47;">${siswa.kelas || '-'}</span></td>
|
||||
<td>${username}</td>
|
||||
<td class="text-center">
|
||||
<div class="d-flex justify-content-center gap-2">
|
||||
<a href="${editUrl}" class="btn btn-sm btn-outline-primary shadow-sm" title="Edit Data">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<button onclick="deleteSiswa(${siswa.id})" class="btn btn-sm btn-outline-danger shadow-sm" title="Hapus Data">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
|
||||
controls.appendChild(createButton('<i class="fas fa-chevron-right"></i>', current + 1, current === last));
|
||||
html += `
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div id="paginationWrapper" class="d-flex justify-content-between align-items-center p-3 border-top bg-light mt-3 rounded-bottom"></div>
|
||||
`;
|
||||
|
||||
tableContent.innerHTML = html;
|
||||
}
|
||||
|
||||
function renderPagination(meta) {
|
||||
const paginationWrapper = document.getElementById('paginationWrapper');
|
||||
if (!paginationWrapper) return;
|
||||
|
||||
let html = `<div class="text-muted small fw-bold">Menampilkan ${meta.from || 0} - ${meta.to || 0} dari ${meta.total || 0} Data</div>`;
|
||||
html += `<div class="d-flex gap-2">`;
|
||||
|
||||
if (meta.current_page > 1) {
|
||||
html += `<button onclick="fetchSiswa(${meta.current_page - 1})" class="btn btn-sm btn-light border fw-bold text-muted shadow-sm"><i class="fas fa-chevron-left me-1"></i> Sebelumnya</button>`;
|
||||
}
|
||||
|
||||
if (meta.current_page < meta.last_page) {
|
||||
html += `<button onclick="fetchSiswa(${meta.current_page + 1})" class="btn btn-sm btn-light border fw-bold text-muted shadow-sm">Selanjutnya <i class="fas fa-chevron-right ms-1"></i></button>`;
|
||||
}
|
||||
|
||||
html += `</div>`;
|
||||
paginationWrapper.innerHTML = html;
|
||||
}
|
||||
|
||||
async function deleteSiswa(id) {
|
||||
const result = await Swal.fire({
|
||||
const confirmResult = await Swal.fire({
|
||||
title: 'Hapus data?',
|
||||
text: 'Data siswa akan dihapus permanen!',
|
||||
text: "Data yang dihapus tidak dapat dikembalikan!",
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#ef4444',
|
||||
cancelButtonColor: '#64748b',
|
||||
confirmButtonText: 'Ya, Hapus',
|
||||
confirmButtonColor: '#d33',
|
||||
cancelButtonColor: '#3085d6',
|
||||
confirmButtonText: 'Ya, Hapus!',
|
||||
cancelButtonText: 'Batal'
|
||||
});
|
||||
|
||||
if (!result.isConfirmed) return;
|
||||
|
||||
const token = localStorage.getItem('access_token');
|
||||
if (!confirmResult.isConfirmed) return;
|
||||
|
||||
const token = getToken();
|
||||
try {
|
||||
const response = await fetch('/api/admin/siswa/' + id, {
|
||||
const response = await fetch(`${API_SISWA_URL}/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
const resultDelete = await parseJsonResponse(response);
|
||||
|
||||
if (response.ok) {
|
||||
Swal.fire('Berhasil!', 'Data siswa berhasil dihapus.', 'success');
|
||||
if (response.ok && resultDelete.success !== false) {
|
||||
Swal.fire('Terhapus!', resultDelete.message || 'Data berhasil dihapus.', 'success');
|
||||
fetchSiswa(currentPage);
|
||||
} else {
|
||||
Swal.fire('Gagal!', 'Data siswa gagal dihapus.', 'error');
|
||||
Swal.fire('Gagal!', resultDelete.message || 'Gagal menghapus data.', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
Swal.fire('Error!', 'Koneksi ke server gagal.', 'error');
|
||||
Swal.fire('Error!', 'Gagal menghubungi server.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -297,11 +213,11 @@ function renderPagination(data) {
|
|||
searchTimer = setTimeout(() => {
|
||||
currentSearch = this.value.trim();
|
||||
fetchSiswa(1);
|
||||
}, 350);
|
||||
}, 500);
|
||||
});
|
||||
|
||||
document.getElementById('perPageSelect').addEventListener('change', function () {
|
||||
perPage = this.value;
|
||||
perPage = parseInt(this.value);
|
||||
fetchSiswa(1);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,52 +7,73 @@
|
|||
<div class="dashboard-section d-flex justify-content-center">
|
||||
<div style="width: 100%; max-width: 700px;">
|
||||
<div class="mb-4 text-center">
|
||||
<h4 class="fw-bold text-dark">Tambah Siswa Baru</h4>
|
||||
<p class="text-muted small text-uppercase fw-bold" style="letter-spacing: 1px;">Sistem Rekomendasi SMAN 4 Jember</p>
|
||||
<h4 class="fw-bold text-dark">Tambah Data Siswa Baru</h4>
|
||||
<p class="text-muted small">Isi data di bawah ini untuk membuat profil siswa sekaligus akun login sistem.</p>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 shadow-sm rounded-4">
|
||||
<div class="card-body p-5">
|
||||
<form id="createSiswaForm">
|
||||
<form id="formCreateSiswa">
|
||||
<div class="row g-4">
|
||||
<div class="col-md-12 form-group-custom">
|
||||
<label>Nama Lengkap</label>
|
||||
<input type="text" id="nama_lengkap" class="form-control input-custom" required placeholder="Masukkan nama lengkap siswa">
|
||||
<div class="col-12">
|
||||
<div class="text-muted small fw-bold text-uppercase mb-2" style="letter-spacing: 1px;">
|
||||
<i class="fas fa-key me-2"></i>Informasi Akun Login
|
||||
</div>
|
||||
<hr class="mt-0 opacity-25">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 form-group-custom">
|
||||
<label class="fw-bold small text-muted">Username Login</label>
|
||||
<input type="text" id="username" class="form-control input-custom" placeholder="Contoh: siswabaru12" required>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 form-group-custom">
|
||||
<label class="fw-bold small text-muted">Password</label>
|
||||
<input type="password" id="password" class="form-control input-custom" placeholder="Minimal 6 karakter" required>
|
||||
</div>
|
||||
|
||||
<div class="col-12 mt-4">
|
||||
<div class="text-muted small fw-bold text-uppercase mb-2" style="letter-spacing: 1px;">
|
||||
<i class="fas fa-user me-2"></i>Data Profil Siswa
|
||||
</div>
|
||||
<hr class="mt-0 opacity-25">
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 form-group-custom">
|
||||
<label>NISN</label>
|
||||
<input type="text" id="nisn" class="form-control input-custom" required placeholder="Masukkan 10 digit NISN">
|
||||
<label class="fw-bold small text-muted">Nama Lengkap</label>
|
||||
<input type="text" id="nama_lengkap" class="form-control input-custom" placeholder="Masukkan nama sesuai ijazah/rapor" required>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 form-group-custom">
|
||||
<label>Kelas</label>
|
||||
<input type="text" id="kelas" class="form-control input-custom" required placeholder="Contoh: XII IPA 1">
|
||||
<div class="col-md-6 form-group-custom">
|
||||
<label class="fw-bold small text-muted">NISN</label>
|
||||
<input type="text" id="nisn" class="form-control input-custom" placeholder="10 Digit nomor NISN" maxlength="10" required>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 form-group-custom">
|
||||
<label>Peminatan</label>
|
||||
<select id="peminatan" class="form-select input-custom">
|
||||
<option value="" disabled selected>Pilih Peminatan</option>
|
||||
<option value="Saintek">Saintek</option>
|
||||
<option value="Soshum">Soshum</option>
|
||||
<div class="col-md-6 form-group-custom">
|
||||
<label class="fw-bold small text-muted">Kelas</label>
|
||||
<select id="kelas" class="form-select input-custom" required>
|
||||
<option value="" disabled selected>Pilih Kelas...</option>
|
||||
<option value="XII MIPA 1">XII MIPA 1</option>
|
||||
<option value="XII MIPA 2">XII MIPA 2</option>
|
||||
<option value="XII MIPA 3">XII MIPA 3</option>
|
||||
<option value="XII MIPA 4">XII MIPA 4</option>
|
||||
<option value="XII IPS 1">XII IPS 1</option>
|
||||
<option value="XII IPS 2">XII IPS 2</option>
|
||||
<option value="XII IPS 3">XII IPS 3</option>
|
||||
<option value="XII IPS 4">XII IPS 4</option>
|
||||
<option value="XII BAHASA">XII BAHASA</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 form-group-custom">
|
||||
<label>User ID (Sesuai Akun Login)</label>
|
||||
<input type="number" id="user_id" class="form-control input-custom" required placeholder="Contoh: 1">
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 form-group-custom">
|
||||
<label>Jurusan ID</label>
|
||||
<input type="number" id="jurusan_id" class="form-control input-custom" required placeholder="Contoh: 1">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 d-flex gap-2 justify-content-end">
|
||||
<button type="submit" id="btnSave" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">Simpan Data</button>
|
||||
<a href="{{ route('admin.siswa') }}" class="btn btn-light px-4" style="border-radius: 10px;">Batal</a>
|
||||
<button type="submit" id="btnSave" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">
|
||||
Simpan Data
|
||||
</button>
|
||||
|
||||
<a href="{{ route('admin.siswa') }}" class="btn btn-light px-4" style="border-radius: 10px;">
|
||||
Batal
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -61,43 +82,109 @@
|
|||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('createSiswaForm').addEventListener('submit', async (e) => {
|
||||
function getAppBaseUrl() {
|
||||
const origin = window.location.origin;
|
||||
const segments = window.location.pathname.split('/').filter(Boolean);
|
||||
const adminIndex = segments.indexOf('admin');
|
||||
if (adminIndex > 0) {
|
||||
return origin + '/' + segments.slice(0, adminIndex).join('/');
|
||||
}
|
||||
return origin;
|
||||
}
|
||||
|
||||
const APP_BASE_URL = getAppBaseUrl();
|
||||
const API_SISWA_URL = `${APP_BASE_URL}/api/admin/siswa`;
|
||||
const LOGIN_URL = `${APP_BASE_URL}/login`;
|
||||
const SISWA_INDEX_URL = `${APP_BASE_URL}/admin/siswa`;
|
||||
|
||||
function getToken() {
|
||||
return localStorage.getItem('access_token') || localStorage.getItem('token') || '';
|
||||
}
|
||||
|
||||
function getValue(id) {
|
||||
return document.getElementById(id).value.trim();
|
||||
}
|
||||
|
||||
async function parseJsonResponse(response) {
|
||||
const text = await response.text();
|
||||
try {
|
||||
return text ? JSON.parse(text) : {};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Respon server tidak valid.'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function formatErrors(errors) {
|
||||
if (!errors) return '';
|
||||
return Object.values(errors).flat().map(item => String(item)).join('<br>');
|
||||
}
|
||||
|
||||
document.getElementById('formCreateSiswa').addEventListener('submit', async function (e) {
|
||||
e.preventDefault();
|
||||
const token = getToken();
|
||||
|
||||
if (!token) {
|
||||
Swal.fire('Sesi Berakhir', 'Silakan login ulang.', 'warning')
|
||||
.then(() => window.location.href = LOGIN_URL);
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.getElementById('btnSave');
|
||||
btn.disabled = true;
|
||||
btn.innerText = 'Menyimpan...';
|
||||
|
||||
const payload = {
|
||||
user_id: document.getElementById('user_id').value,
|
||||
jurusan_id: document.getElementById('jurusan_id').value,
|
||||
nama_lengkap: document.getElementById('nama_lengkap').value,
|
||||
nisn: document.getElementById('nisn').value,
|
||||
kelas: document.getElementById('kelas').value,
|
||||
peminatan: document.getElementById('peminatan').value,
|
||||
username: getValue('username'),
|
||||
password: getValue('password'),
|
||||
nama_lengkap: getValue('nama_lengkap'),
|
||||
nisn: getValue('nisn'),
|
||||
kelas: getValue('kelas')
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/siswa', {
|
||||
const response = await fetch(API_SISWA_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('access_token'),
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
const result = await parseJsonResponse(response);
|
||||
|
||||
if (response.ok) {
|
||||
Swal.fire({ icon: 'success', title: 'Berhasil!', text: 'Data siswa ditambahkan.', confirmButtonColor: '#2D7A47' })
|
||||
.then(() => { window.location.href = "{{ route('admin.siswa') }}"; });
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
Swal.fire('Sesi Berakhir', 'Silakan login ulang.', 'warning')
|
||||
.then(() => window.location.href = LOGIN_URL);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.ok && result.success !== false) {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Berhasil!',
|
||||
text: result.message || 'Data siswa berhasil ditambahkan.',
|
||||
confirmButtonColor: '#2D6A4F'
|
||||
}).then(() => {
|
||||
window.location.href = SISWA_INDEX_URL;
|
||||
});
|
||||
} else {
|
||||
Swal.fire('Gagal!', result.message || 'Cek kembali data Anda.', 'warning');
|
||||
Swal.fire({
|
||||
icon: 'warning',
|
||||
title: 'Gagal!',
|
||||
html: formatErrors(result.errors) || result.message || 'Periksa kembali isian Anda.',
|
||||
confirmButtonColor: '#2D6A4F'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
Swal.fire('Error!', 'Gagal menghubungi server.', 'error');
|
||||
Swal.fire('Error!', error.message || 'Gagal menghubungi server.', 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerText = 'Simpan Data';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -7,43 +7,76 @@
|
|||
<div class="dashboard-section d-flex justify-content-center">
|
||||
<div style="width: 100%; max-width: 700px;">
|
||||
<div class="mb-4 text-center">
|
||||
<h4 class="fw-bold text-dark">Edit Data Siswa</h4>
|
||||
<p id="display_id_siswa" class="text-muted small fw-bold">Memuat data...</p>
|
||||
<h4 class="fw-bold text-dark">Edit Data Profil Siswa</h4>
|
||||
<p class="text-muted small">Ubah rincian informasi dan akun profil siswa.</p>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 shadow-sm rounded-4">
|
||||
<div class="card-body p-5">
|
||||
<form id="editSiswaForm">
|
||||
<input type="hidden" id="siswa_id_db" value="{{ $id }}">
|
||||
<form id="formEditSiswa">
|
||||
<input type="hidden" id="siswa_id" value="{{ $id }}">
|
||||
|
||||
<div class="row g-4">
|
||||
|
||||
<div class="col-12">
|
||||
<div class="text-muted small fw-bold text-uppercase mb-2" style="letter-spacing: 1px;">
|
||||
<i class="fas fa-key me-2"></i>Informasi Akun Login
|
||||
</div>
|
||||
<hr class="mt-0 opacity-25">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 form-group-custom">
|
||||
<label class="fw-bold small text-muted">Username Login (Opsional)</label>
|
||||
<input type="text" id="username" class="form-control input-custom" placeholder="Kosongkan jika tidak diubah">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 form-group-custom">
|
||||
<label class="fw-bold small text-muted">Password Baru (Opsional)</label>
|
||||
<input type="password" id="password" class="form-control input-custom" placeholder="Kosongkan jika tidak diubah">
|
||||
</div>
|
||||
|
||||
<div class="col-12 mt-4">
|
||||
<div class="text-muted small fw-bold text-uppercase mb-2" style="letter-spacing: 1px;">
|
||||
<i class="fas fa-user me-2"></i>Data Profil Siswa
|
||||
</div>
|
||||
<hr class="mt-0 opacity-25">
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 form-group-custom">
|
||||
<label>Nama Lengkap</label>
|
||||
<label class="fw-bold small text-muted">Nama Lengkap</label>
|
||||
<input type="text" id="nama_lengkap" class="form-control input-custom" required>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 form-group-custom">
|
||||
<label>NISN</label>
|
||||
<input type="text" id="nisn" class="form-control input-custom" required>
|
||||
<div class="col-md-6 form-group-custom">
|
||||
<label class="fw-bold small text-muted">NISN</label>
|
||||
<input type="text" id="nisn" class="form-control input-custom" maxlength="10" required>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 form-group-custom">
|
||||
<label>Kelas</label>
|
||||
<input type="text" id="kelas" class="form-control input-custom" required>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 form-group-custom">
|
||||
<label>Peminatan</label>
|
||||
<select id="peminatan" class="form-select input-custom">
|
||||
<option value="Saintek">Saintek</option>
|
||||
<option value="Soshum">Soshum</option>
|
||||
<div class="col-md-6 form-group-custom">
|
||||
<label class="fw-bold small text-muted">Kelas</label>
|
||||
<select id="kelas" class="form-select input-custom" required>
|
||||
<option value="" disabled selected>Pilih Kelas...</option>
|
||||
<option value="XII MIPA 1">XII MIPA 1</option>
|
||||
<option value="XII MIPA 2">XII MIPA 2</option>
|
||||
<option value="XII MIPA 3">XII MIPA 3</option>
|
||||
<option value="XII MIPA 4">XII MIPA 4</option>
|
||||
<option value="XII IPS 1">XII IPS 1</option>
|
||||
<option value="XII IPS 2">XII IPS 2</option>
|
||||
<option value="XII IPS 3">XII IPS 3</option>
|
||||
<option value="XII IPS 4">XII IPS 4</option>
|
||||
<option value="XII BAHASA">XII BAHASA</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 d-flex gap-2 justify-content-end">
|
||||
<button type="submit" id="btnUpdate" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">Perbarui Data</button>
|
||||
<a href="{{ route('admin.siswa') }}" class="btn btn-light px-4" style="border-radius: 10px;">Batal</a>
|
||||
<button type="submit" id="btnUpdate" class="btn text-white px-5 shadow-sm" style="background-color: var(--primary-green); border-radius: 10px;">
|
||||
Perbarui Data
|
||||
</button>
|
||||
|
||||
<a href="{{ route('admin.siswa') }}" class="btn btn-light px-4" style="border-radius: 10px;">
|
||||
Batal
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -52,71 +85,134 @@
|
|||
</div>
|
||||
|
||||
<script>
|
||||
const dbId = document.getElementById('siswa_id_db').value;
|
||||
function getAppBaseUrl() {
|
||||
const origin = window.location.origin;
|
||||
const segments = window.location.pathname.split('/').filter(Boolean);
|
||||
const adminIndex = segments.indexOf('admin');
|
||||
if (adminIndex > 0) {
|
||||
return origin + '/' + segments.slice(0, adminIndex).join('/');
|
||||
}
|
||||
return origin;
|
||||
}
|
||||
|
||||
// Mengambil data lama untuk ditampilkan di form
|
||||
async function loadSiswa() {
|
||||
const APP_BASE_URL = getAppBaseUrl();
|
||||
const API_SISWA_URL = `${APP_BASE_URL}/api/admin/siswa`;
|
||||
const LOGIN_URL = `${APP_BASE_URL}/login`;
|
||||
const SISWA_INDEX_URL = `${APP_BASE_URL}/admin/siswa`;
|
||||
|
||||
function getToken() {
|
||||
return localStorage.getItem('access_token') || localStorage.getItem('token') || '';
|
||||
}
|
||||
|
||||
async function parseJsonResponse(response) {
|
||||
const text = await response.text();
|
||||
try {
|
||||
const response = await fetch(`/api/admin/siswa/${dbId}`, {
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('access_token'),
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
const result = await response.json();
|
||||
if (response.ok) {
|
||||
document.getElementById('nama_lengkap').value = result.data.nama_lengkap;
|
||||
document.getElementById('nisn').value = result.data.nisn;
|
||||
document.getElementById('kelas').value = result.data.kelas;
|
||||
document.getElementById('peminatan').value = result.data.peminatan;
|
||||
document.getElementById('display_id_siswa').innerText = `EDITING: ${result.data.id_siswa}`;
|
||||
} else {
|
||||
Swal.fire('Gagal!', 'Data tidak ditemukan.', 'warning');
|
||||
}
|
||||
return text ? JSON.parse(text) : {};
|
||||
} catch (error) {
|
||||
Swal.fire('Error!', 'Gagal mengambil data dari server.', 'error');
|
||||
return { success: false, message: 'Respon server tidak valid.' };
|
||||
}
|
||||
}
|
||||
|
||||
// Mengirim data perbaikan ke API
|
||||
document.getElementById('editSiswaForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const btn = document.getElementById('btnUpdate');
|
||||
btn.disabled = true;
|
||||
document.addEventListener('DOMContentLoaded', async function () {
|
||||
const id = document.getElementById('siswa_id').value;
|
||||
const token = getToken();
|
||||
|
||||
const payload = {
|
||||
nama_lengkap: document.getElementById('nama_lengkap').value,
|
||||
nisn: document.getElementById('nisn').value,
|
||||
kelas: document.getElementById('kelas').value,
|
||||
peminatan: document.getElementById('peminatan').value
|
||||
};
|
||||
if (!token) {
|
||||
window.location.href = LOGIN_URL;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/admin/siswa/${dbId}`, {
|
||||
const response = await fetch(`${API_SISWA_URL}/${id}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const result = await parseJsonResponse(response);
|
||||
|
||||
if (response.ok && result.success) {
|
||||
const s = result.data;
|
||||
document.getElementById('nama_lengkap').value = s.nama_lengkap || '';
|
||||
document.getElementById('nisn').value = s.nisn || '';
|
||||
|
||||
if (s.kelas) {
|
||||
document.getElementById('kelas').value = s.kelas;
|
||||
}
|
||||
|
||||
if (s.user && s.user.username) {
|
||||
document.getElementById('username').placeholder = `Username saat ini: ${s.user.username}`;
|
||||
}
|
||||
} else {
|
||||
Swal.fire('Gagal!', result.message || 'Gagal memuat rincian data siswa.', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
Swal.fire('Error!', 'Gagal menghubungi server untuk memuat data.', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('formEditSiswa').addEventListener('submit', async function (e) {
|
||||
e.preventDefault();
|
||||
const id = document.getElementById('siswa_id').value;
|
||||
const btn = document.getElementById('btnUpdate');
|
||||
|
||||
btn.disabled = true;
|
||||
btn.innerText = 'Memperbarui...';
|
||||
|
||||
const payload = {
|
||||
nama_lengkap: document.getElementById('nama_lengkap').value.trim(),
|
||||
nisn: document.getElementById('nisn').value.trim(),
|
||||
kelas: document.getElementById('kelas').value
|
||||
};
|
||||
|
||||
const passwordInput = document.getElementById('password').value.trim();
|
||||
if (passwordInput.length > 0) {
|
||||
payload.password = passwordInput;
|
||||
}
|
||||
|
||||
const usernameInput = document.getElementById('username').value.trim();
|
||||
if (usernameInput.length > 0) {
|
||||
payload.username = usernameInput;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_SISWA_URL}/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + getToken(),
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('access_token'),
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
Swal.fire({ icon: 'success', title: 'Berhasil Diperbarui!', text: 'Data siswa telah diperbarui.', confirmButtonColor: '#2D7A47' })
|
||||
.then(() => { window.location.href = "{{ route('admin.siswa') }}"; });
|
||||
const result = await parseJsonResponse(response);
|
||||
|
||||
if (response.ok && result.success !== false) {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Berhasil Diperbarui!',
|
||||
text: result.message || 'Data siswa berhasil diperbarui.',
|
||||
confirmButtonColor: '#2D7A47'
|
||||
}).then(() => {
|
||||
window.location.href = SISWA_INDEX_URL;
|
||||
});
|
||||
} else {
|
||||
const result = await response.json();
|
||||
Swal.fire('Gagal!', result.message || 'Cek inputan.', 'warning');
|
||||
Swal.fire({
|
||||
icon: 'warning',
|
||||
title: 'Gagal!',
|
||||
text: result.message || 'Cek kembali rincian data yang Anda ubah.',
|
||||
confirmButtonColor: '#2D7A47'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
Swal.fire('Error!', 'Kesalahan koneksi server.', 'error');
|
||||
Swal.fire('Error!', 'Gagal menghubungi server.', 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerText = 'Perbarui Data';
|
||||
}
|
||||
});
|
||||
|
||||
// Jalankan load data saat halaman siap
|
||||
document.addEventListener('DOMContentLoaded', loadSiswa);
|
||||
</script>
|
||||
@endsection
|
||||
|
|
@ -75,20 +75,26 @@ class="input-modern w-full text-slate-900"
|
|||
</div>
|
||||
|
||||
<script>
|
||||
const apiLoginUrl = @json(route('api.login'));
|
||||
const adminDashboardUrl = @json(route('admin.dashboard'));
|
||||
const studentDashboardUrl = @json(route('student.profile'));
|
||||
|
||||
document.getElementById('loginForm').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const btn = document.getElementById('btnLogin');
|
||||
const username = document.getElementById('username').value;
|
||||
const username = document.getElementById('username').value.trim();
|
||||
const password = document.getElementById('password').value;
|
||||
const errorElement = document.getElementById('error-message');
|
||||
|
||||
errorElement.classList.add('hidden');
|
||||
errorElement.innerText = '';
|
||||
|
||||
btn.disabled = true;
|
||||
btn.innerText = 'Memproses...';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/login', {
|
||||
const response = await fetch(apiLoginUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
|
@ -100,16 +106,32 @@ class="input-modern w-full text-slate-900"
|
|||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
let data = {};
|
||||
|
||||
if (response.ok) {
|
||||
if (contentType.includes('application/json')) {
|
||||
data = await response.json();
|
||||
} else {
|
||||
const text = await response.text();
|
||||
console.error('Response bukan JSON:', text);
|
||||
throw new Error('Server tidak mengembalikan JSON.');
|
||||
}
|
||||
|
||||
if (response.ok && data.success) {
|
||||
localStorage.setItem('access_token', data.access_token);
|
||||
localStorage.setItem('user_role', data.role);
|
||||
|
||||
if (data.user) {
|
||||
localStorage.setItem('user_name', data.user.name || data.user.username || 'User');
|
||||
}
|
||||
|
||||
if (data.role === 'admin') {
|
||||
window.location.href = "{{ route('admin.dashboard') }}";
|
||||
window.location.href = adminDashboardUrl;
|
||||
} else if (data.role === 'siswa') {
|
||||
window.location.href = studentDashboardUrl;
|
||||
} else {
|
||||
window.location.href = "{{ route('student.profile') }}";
|
||||
errorElement.innerText = 'Role pengguna tidak dikenali.';
|
||||
errorElement.classList.remove('hidden');
|
||||
}
|
||||
} else {
|
||||
errorElement.innerText = data.message || 'Username atau password salah.';
|
||||
|
|
|
|||
|
|
@ -107,13 +107,13 @@
|
|||
|
||||
<body class="flex flex-col min-h-screen text-slate-800">
|
||||
|
||||
<!-- Navbar -->
|
||||
<nav class="sticky top-0 z-50 bg-white/95 backdrop-blur-md border-b border-slate-100 px-6 py-4">
|
||||
<div class="max-w-7xl mx-auto flex justify-between items-center gap-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-10 h-10 bg-emerald-100 rounded-xl flex items-center justify-center text-emerald-600 font-bold">
|
||||
AS
|
||||
</div>
|
||||
|
||||
<span class="font-black text-xl tracking-tighter text-emerald-950 uppercase italic">
|
||||
ASGARD.
|
||||
</span>
|
||||
|
|
@ -129,19 +129,15 @@
|
|||
Cek Rekomendasi
|
||||
</a>
|
||||
|
||||
<!-- Menu ini membuka hasil rekomendasi terakhir -->
|
||||
<a href="#" onclick="goToLastResult(event)" class="hover:text-emerald-600 transition">
|
||||
Hasil Rekomendasi
|
||||
</a>
|
||||
|
||||
<!-- <a href="#" class="hover:text-emerald-600 transition">
|
||||
Kontak BK
|
||||
</a> -->
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="hidden sm:flex items-center gap-3 bg-emerald-50 px-4 py-2 rounded-full border border-emerald-100">
|
||||
<div class="w-2 h-2 bg-emerald-500 rounded-full animate-pulse"></div>
|
||||
|
||||
<span id="student-name-label" class="text-[10px] font-bold text-emerald-700 uppercase">
|
||||
Siswa
|
||||
</span>
|
||||
|
|
@ -159,7 +155,6 @@
|
|||
|
||||
<main class="max-w-7xl mx-auto px-6 flex-grow py-10">
|
||||
|
||||
<!-- Header -->
|
||||
<header class="mb-10">
|
||||
<h1 class="text-4xl font-extrabold text-slate-900 tracking-tight">
|
||||
Halo, Siswa SMAPA! 👋
|
||||
|
|
@ -170,7 +165,6 @@
|
|||
</p>
|
||||
</header>
|
||||
|
||||
<!-- Statistik -->
|
||||
<section class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-10">
|
||||
<div class="bg-white p-8 rounded-3xl card-shadow border border-slate-100 card-hover">
|
||||
<h4 class="text-slate-400 text-xs font-bold uppercase tracking-widest mb-4">
|
||||
|
|
@ -213,7 +207,6 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Akses cepat -->
|
||||
<section class="bg-white p-8 md:p-10 rounded-[2.5rem] card-shadow border border-slate-100 mb-10">
|
||||
<div class="mb-8">
|
||||
<h3 class="text-xl font-black text-slate-900">
|
||||
|
|
@ -236,6 +229,7 @@ class="flex items-center p-6 bg-blue-50 rounded-2xl border border-blue-100 card-
|
|||
<h4 class="font-black text-blue-900 text-sm">
|
||||
Cek Rekomendasi Baru
|
||||
</h4>
|
||||
|
||||
<p class="text-[11px] text-blue-700 font-semibold mt-1">
|
||||
Isi data kembali untuk mendapatkan hasil terbaru.
|
||||
</p>
|
||||
|
|
@ -252,6 +246,7 @@ class="flex items-center p-6 bg-emerald-50 rounded-2xl border border-emerald-100
|
|||
<h4 class="font-black text-emerald-900 text-sm">
|
||||
Hasil Rekomendasi
|
||||
</h4>
|
||||
|
||||
<p class="text-[11px] text-emerald-700 font-semibold mt-1">
|
||||
Buka kembali hasil rekomendasi terakhir.
|
||||
</p>
|
||||
|
|
@ -268,6 +263,7 @@ class="flex items-center p-6 bg-violet-50 rounded-2xl border border-violet-100 c
|
|||
<h4 class="font-black text-violet-900 text-sm">
|
||||
Riwayat Rekomendasi
|
||||
</h4>
|
||||
|
||||
<p class="text-[11px] text-violet-700 font-semibold mt-1">
|
||||
Lihat daftar rekomendasi yang pernah dibuat.
|
||||
</p>
|
||||
|
|
@ -276,7 +272,6 @@ class="flex items-center p-6 bg-violet-50 rounded-2xl border border-violet-100 c
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Grafik -->
|
||||
<section class="bg-white p-8 md:p-10 rounded-[2.5rem] card-shadow border border-slate-100 mb-10">
|
||||
<h3 class="text-xl font-black text-slate-900 mb-2">
|
||||
Perkembangan Rata-Rata Nilai
|
||||
|
|
@ -295,7 +290,6 @@ class="flex items-center p-6 bg-violet-50 rounded-2xl border border-violet-100 c
|
|||
</p>
|
||||
</section>
|
||||
|
||||
<!-- Panduan -->
|
||||
<section id="panduan-section" class="bg-white p-8 md:p-10 rounded-[2.5rem] card-shadow border border-slate-100 mb-10">
|
||||
<div class="flex flex-col lg:flex-row gap-8">
|
||||
<div class="lg:w-1/3">
|
||||
|
|
@ -346,7 +340,6 @@ class="flex items-center p-6 bg-violet-50 rounded-2xl border border-violet-100 c
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Riwayat -->
|
||||
<section id="riwayat-section" class="bg-white p-8 md:p-10 rounded-[2.5rem] card-shadow border border-slate-100 mb-16">
|
||||
<div class="mb-6">
|
||||
<h3 class="text-xl font-black text-slate-900">
|
||||
|
|
@ -366,7 +359,6 @@ class="flex items-center p-6 bg-violet-50 rounded-2xl border border-violet-100 c
|
|||
</section>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="bg-[#062C22] text-white py-16 px-6">
|
||||
<div class="max-w-7xl mx-auto grid grid-cols-1 md:grid-cols-3 gap-12">
|
||||
<div class="space-y-6">
|
||||
|
|
@ -390,11 +382,6 @@ class="flex items-center p-6 bg-violet-50 rounded-2xl border border-violet-100 c
|
|||
Panduan Sistem
|
||||
</a>
|
||||
</li>
|
||||
<!-- <li>
|
||||
<a href="#" class="hover:text-white transition">
|
||||
Kebijakan Privasi
|
||||
</a>
|
||||
</li> -->
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
|
@ -421,17 +408,57 @@ class="flex items-center p-6 bg-violet-50 rounded-2xl border border-violet-100 c
|
|||
<script>
|
||||
let trendChartInstance = null;
|
||||
|
||||
const routeLogin = @json(route('login'));
|
||||
const routeDashboard = @json(route('student.profile'));
|
||||
const routePrediction = @json(route('student.prediction'));
|
||||
const routeAdminDashboard = @json(route('admin.dashboard'));
|
||||
function getAppBaseUrl() {
|
||||
const origin = window.location.origin;
|
||||
const segments = window.location.pathname.split('/').filter(Boolean);
|
||||
const studentIndex = segments.indexOf('student');
|
||||
|
||||
if (studentIndex > 0) {
|
||||
return origin + '/' + segments.slice(0, studentIndex).join('/');
|
||||
}
|
||||
|
||||
return origin;
|
||||
}
|
||||
|
||||
const APP_BASE_URL = getAppBaseUrl();
|
||||
|
||||
const routeLogin = `${APP_BASE_URL}/login`;
|
||||
const routeDashboard = `${APP_BASE_URL}/student/profile`;
|
||||
const routePrediction = `${APP_BASE_URL}/student/prediction`;
|
||||
const routeAdminDashboard = `${APP_BASE_URL}/admin/dashboard`;
|
||||
|
||||
const apiLogoutUrl = `${APP_BASE_URL}/api/logout`;
|
||||
const apiStudentDashboardStatsUrl = `${APP_BASE_URL}/api/student/dashboard-stats`;
|
||||
const studentResultUrlTemplate = `${APP_BASE_URL}/student/result/__ID__`;
|
||||
|
||||
function getAccessToken() {
|
||||
return localStorage.getItem('access_token');
|
||||
return localStorage.getItem('access_token') ||
|
||||
localStorage.getItem('token') ||
|
||||
sessionStorage.getItem('access_token') ||
|
||||
sessionStorage.getItem('token') ||
|
||||
'';
|
||||
}
|
||||
|
||||
function getUserRole() {
|
||||
return localStorage.getItem('user_role');
|
||||
return localStorage.getItem('user_role') ||
|
||||
localStorage.getItem('role') ||
|
||||
sessionStorage.getItem('user_role') ||
|
||||
sessionStorage.getItem('role') ||
|
||||
'';
|
||||
}
|
||||
|
||||
function clearLoginStorage() {
|
||||
localStorage.removeItem('access_token');
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user_role');
|
||||
localStorage.removeItem('role');
|
||||
localStorage.removeItem('user_name');
|
||||
localStorage.removeItem('name');
|
||||
|
||||
sessionStorage.removeItem('access_token');
|
||||
sessionStorage.removeItem('token');
|
||||
sessionStorage.removeItem('user_role');
|
||||
sessionStorage.removeItem('role');
|
||||
}
|
||||
|
||||
function redirectByRole(role) {
|
||||
|
|
@ -440,7 +467,7 @@ function redirectByRole(role) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (role === 'siswa') {
|
||||
if (role === 'siswa' || role === 'student') {
|
||||
window.location.href = routeDashboard;
|
||||
return;
|
||||
}
|
||||
|
|
@ -448,6 +475,37 @@ function redirectByRole(role) {
|
|||
window.location.href = routeLogin;
|
||||
}
|
||||
|
||||
async function parseJsonResponse(response) {
|
||||
const text = await response.text();
|
||||
|
||||
try {
|
||||
return text ? JSON.parse(text) : {};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: text && text.trim().startsWith('<')
|
||||
? 'Server mengembalikan halaman HTML, bukan JSON. Biasanya URL API salah atau route API belum terdaftar.'
|
||||
: (text || response.statusText || 'Respon server tidak valid.')
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeAppUrl(url) {
|
||||
if (!url) return '';
|
||||
|
||||
const text = String(url);
|
||||
|
||||
if (text.startsWith('http://') || text.startsWith('https://')) {
|
||||
return text;
|
||||
}
|
||||
|
||||
if (text.startsWith('/')) {
|
||||
return APP_BASE_URL + text;
|
||||
}
|
||||
|
||||
return `${APP_BASE_URL}/${text}`;
|
||||
}
|
||||
|
||||
function getStoredLastResult() {
|
||||
try {
|
||||
const saved = localStorage.getItem('asgard_last_result');
|
||||
|
|
@ -466,7 +524,7 @@ function saveStoredLastResult(data) {
|
|||
id: data.id || oldData.id || '',
|
||||
jurusan: data.jurusan || oldData.jurusan || '',
|
||||
tanggal: data.tanggal || oldData.tanggal || '',
|
||||
url: data.url || oldData.url || ''
|
||||
url: normalizeAppUrl(data.url || oldData.url || '')
|
||||
};
|
||||
|
||||
localStorage.setItem('asgard_last_result', JSON.stringify(nextData));
|
||||
|
|
@ -478,12 +536,12 @@ function goToLastResult(event) {
|
|||
const saved = getStoredLastResult();
|
||||
|
||||
if (saved && saved.url) {
|
||||
window.location.href = saved.url;
|
||||
window.location.href = normalizeAppUrl(saved.url);
|
||||
return;
|
||||
}
|
||||
|
||||
if (saved && saved.id) {
|
||||
window.location.href = `/student/result/${saved.id}`;
|
||||
window.location.href = makeResultUrl(saved.id);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -504,7 +562,7 @@ function getHistoryId(item) {
|
|||
}
|
||||
|
||||
function makeResultUrl(id) {
|
||||
return id ? `/student/result/${id}` : '';
|
||||
return id ? studentResultUrlTemplate.replace('__ID__', encodeURIComponent(id)) : '';
|
||||
}
|
||||
|
||||
function isValidRecommendationName(value) {
|
||||
|
|
@ -524,7 +582,7 @@ function isValidRecommendationName(value) {
|
|||
|
||||
if (token) {
|
||||
try {
|
||||
await fetch('/api/logout', {
|
||||
await fetch(apiLogoutUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
|
|
@ -536,8 +594,7 @@ function isValidRecommendationName(value) {
|
|||
}
|
||||
}
|
||||
|
||||
localStorage.removeItem('access_token');
|
||||
localStorage.removeItem('user_role');
|
||||
clearLoginStorage();
|
||||
localStorage.removeItem('asgard_last_result');
|
||||
window.location.href = routeLogin;
|
||||
}
|
||||
|
|
@ -691,7 +748,7 @@ function renderTrendChart(tren) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (role !== 'siswa') {
|
||||
if (role !== 'siswa' && role !== 'student') {
|
||||
redirectByRole(role);
|
||||
return;
|
||||
}
|
||||
|
|
@ -700,28 +757,24 @@ function renderTrendChart(tren) {
|
|||
document.getElementById('student-name-label').innerText = savedName;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/student/dashboard-stats', {
|
||||
const response = await fetch(apiStudentDashboardStatsUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.status === 401) {
|
||||
localStorage.removeItem('access_token');
|
||||
localStorage.removeItem('user_role');
|
||||
const result = await parseJsonResponse(response);
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
clearLoginStorage();
|
||||
window.location.href = routeLogin;
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
document.getElementById('stat-total').innerText = 0;
|
||||
document.getElementById('stat-last').innerText = 'Belum ada rekomendasi';
|
||||
renderActivityList([]);
|
||||
renderTrendChart([]);
|
||||
return;
|
||||
if (!response.ok || result.success === false) {
|
||||
throw new Error(result.message || 'Gagal memuat data dashboard.');
|
||||
}
|
||||
|
||||
const data = result.data || {};
|
||||
|
|
@ -736,7 +789,6 @@ function renderTrendChart(tren) {
|
|||
? lastName
|
||||
: 'Belum ada rekomendasi';
|
||||
|
||||
// Simpan hasil terakhir dari data dashboard jika API mengirim id riwayat
|
||||
const firstId = getHistoryId(firstActivity);
|
||||
|
||||
if (isValidRecommendationName(lastName) && firstId) {
|
||||
|
|
|
|||
|
|
@ -100,13 +100,13 @@
|
|||
|
||||
<body class="flex flex-col min-h-screen text-slate-800">
|
||||
|
||||
<!-- Navbar -->
|
||||
<nav class="sticky top-0 z-50 bg-white/95 backdrop-blur-md border-b border-slate-100 px-6 py-4">
|
||||
<div class="max-w-7xl mx-auto flex items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-10 h-10 bg-emerald-100 rounded-xl flex items-center justify-center text-emerald-600 font-bold">
|
||||
AS
|
||||
</div>
|
||||
|
||||
<span class="font-black text-xl tracking-tighter text-emerald-950 uppercase italic">
|
||||
ASGARD.
|
||||
</span>
|
||||
|
|
@ -121,19 +121,15 @@
|
|||
Cek Rekomendasi
|
||||
</a>
|
||||
|
||||
<!-- Menu aktif halaman hasil -->
|
||||
<a href="#" onclick="goToLastResult(event)" class="text-emerald-600">
|
||||
Hasil Rekomendasi
|
||||
</a>
|
||||
|
||||
<!-- <a href="#" class="hover:text-emerald-600 transition">
|
||||
Kontak BK
|
||||
</a> -->
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="hidden sm:flex items-center gap-3 bg-emerald-50 px-4 py-2 rounded-full border border-emerald-100">
|
||||
<div class="w-2 h-2 bg-emerald-500 rounded-full animate-pulse"></div>
|
||||
|
||||
<span class="text-[10px] font-bold text-emerald-700 uppercase">
|
||||
Siswa
|
||||
</span>
|
||||
|
|
@ -151,7 +147,6 @@
|
|||
<main class="flex-grow px-6 py-10">
|
||||
<div class="max-w-5xl mx-auto">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="text-center mb-8">
|
||||
<div class="inline-flex items-center gap-2 bg-emerald-100 text-emerald-700 px-4 py-2 rounded-full text-xs font-bold">
|
||||
Hasil rekomendasi berhasil diproses ✅
|
||||
|
|
@ -166,7 +161,6 @@
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Hasil utama -->
|
||||
<section class="bg-gradient-to-br from-emerald-600 via-emerald-500 to-teal-600 rounded-[2.5rem] p-8 md:p-14 text-white card-shadow mb-8 relative overflow-hidden">
|
||||
<div class="absolute inset-0 opacity-10">
|
||||
<div class="absolute -top-10 -left-10 w-40 h-40 bg-white rounded-full blur-3xl"></div>
|
||||
|
|
@ -187,7 +181,6 @@
|
|||
</p>
|
||||
|
||||
<div class="flex flex-col sm:flex-row gap-3 justify-center">
|
||||
<!-- Tombol kembali, hasil tetap tersimpan di localStorage -->
|
||||
<a href="{{ route('student.profile') }}"
|
||||
onclick="saveLastRecommendationResult()"
|
||||
class="inline-flex items-center justify-center bg-white hover:bg-slate-100 text-emerald-700 font-bold py-4 px-8 rounded-2xl transition">
|
||||
|
|
@ -203,7 +196,6 @@ class="inline-flex items-center justify-center bg-emerald-800/50 hover:bg-emeral
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Rekapan data -->
|
||||
<section class="bg-white rounded-[2rem] border border-slate-100 card-shadow p-6 md:p-8 mb-8">
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<div class="w-10 h-10 rounded-2xl bg-violet-100 flex items-center justify-center text-xl">
|
||||
|
|
@ -272,7 +264,6 @@ class="inline-flex items-center justify-center bg-emerald-800/50 hover:bg-emeral
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Informasi hasil -->
|
||||
<section class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||||
<div class="bg-white rounded-[2rem] border border-slate-100 card-shadow p-6 md:p-8">
|
||||
<h3 class="text-xl font-extrabold text-slate-800 mb-4">
|
||||
|
|
@ -291,13 +282,12 @@ class="inline-flex items-center justify-center bg-emerald-800/50 hover:bg-emeral
|
|||
Peluang Karier
|
||||
</h3>
|
||||
|
||||
<p class="text-slate-600 leading-relaxed">
|
||||
{{ $prospekKerja }}
|
||||
</p>
|
||||
<div class="text-slate-600 leading-relaxed">
|
||||
{!! $prospekKerja !!}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Saran -->
|
||||
<section class="bg-white rounded-[2rem] border border-slate-100 card-shadow p-6 md:p-8">
|
||||
<h3 class="text-xl font-extrabold text-slate-800 mb-5">
|
||||
Langkah Setelah Mendapatkan Hasil
|
||||
|
|
@ -338,7 +328,6 @@ class="inline-flex items-center justify-center bg-emerald-800/50 hover:bg-emeral
|
|||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="bg-[#062C22] text-white py-16 px-6 mt-16">
|
||||
<div class="max-w-7xl mx-auto grid grid-cols-1 md:grid-cols-3 gap-12">
|
||||
<div class="space-y-6">
|
||||
|
|
@ -362,11 +351,6 @@ class="inline-flex items-center justify-center bg-emerald-800/50 hover:bg-emeral
|
|||
Panduan Sistem
|
||||
</a>
|
||||
</li>
|
||||
<!-- <li>
|
||||
<a href="#" class="hover:text-white transition">
|
||||
Kebijakan Privasi
|
||||
</a>
|
||||
</li> -->
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
|
@ -391,17 +375,55 @@ class="inline-flex items-center justify-center bg-emerald-800/50 hover:bg-emeral
|
|||
</footer>
|
||||
|
||||
<script>
|
||||
const routeLogin = @json(route('login'));
|
||||
const routeDashboard = @json(route('student.profile'));
|
||||
const routePrediction = @json(route('student.prediction'));
|
||||
const routeAdminDashboard = @json(route('admin.dashboard'));
|
||||
function getAppBaseUrl() {
|
||||
const origin = window.location.origin;
|
||||
const segments = window.location.pathname.split('/').filter(Boolean);
|
||||
const studentIndex = segments.indexOf('student');
|
||||
|
||||
if (studentIndex > 0) {
|
||||
return origin + '/' + segments.slice(0, studentIndex).join('/');
|
||||
}
|
||||
|
||||
return origin;
|
||||
}
|
||||
|
||||
const APP_BASE_URL = getAppBaseUrl();
|
||||
|
||||
const routeLogin = `${APP_BASE_URL}/login`;
|
||||
const routeDashboard = `${APP_BASE_URL}/student/profile`;
|
||||
const routePrediction = `${APP_BASE_URL}/student/prediction`;
|
||||
const routeAdminDashboard = `${APP_BASE_URL}/admin/dashboard`;
|
||||
const apiLogoutUrl = `${APP_BASE_URL}/api/logout`;
|
||||
const studentResultUrlTemplate = `${APP_BASE_URL}/student/result/__ID__`;
|
||||
|
||||
function getAccessToken() {
|
||||
return localStorage.getItem('access_token');
|
||||
return localStorage.getItem('access_token') ||
|
||||
localStorage.getItem('token') ||
|
||||
sessionStorage.getItem('access_token') ||
|
||||
sessionStorage.getItem('token') ||
|
||||
'';
|
||||
}
|
||||
|
||||
function getUserRole() {
|
||||
return localStorage.getItem('user_role');
|
||||
return localStorage.getItem('user_role') ||
|
||||
localStorage.getItem('role') ||
|
||||
sessionStorage.getItem('user_role') ||
|
||||
sessionStorage.getItem('role') ||
|
||||
'';
|
||||
}
|
||||
|
||||
function clearLoginStorage() {
|
||||
localStorage.removeItem('access_token');
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user_role');
|
||||
localStorage.removeItem('role');
|
||||
localStorage.removeItem('user_name');
|
||||
localStorage.removeItem('name');
|
||||
|
||||
sessionStorage.removeItem('access_token');
|
||||
sessionStorage.removeItem('token');
|
||||
sessionStorage.removeItem('user_role');
|
||||
sessionStorage.removeItem('role');
|
||||
}
|
||||
|
||||
function redirectByRole(role) {
|
||||
|
|
@ -410,7 +432,7 @@ function redirectByRole(role) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (role === 'siswa') {
|
||||
if (role === 'siswa' || role === 'student') {
|
||||
window.location.href = routeDashboard;
|
||||
return;
|
||||
}
|
||||
|
|
@ -418,6 +440,26 @@ function redirectByRole(role) {
|
|||
window.location.href = routeLogin;
|
||||
}
|
||||
|
||||
function makeResultUrl(id) {
|
||||
return id ? studentResultUrlTemplate.replace('__ID__', encodeURIComponent(id)) : '';
|
||||
}
|
||||
|
||||
function normalizeAppUrl(url) {
|
||||
if (!url) return '';
|
||||
|
||||
const text = String(url);
|
||||
|
||||
if (text.startsWith('http://') || text.startsWith('https://')) {
|
||||
return text;
|
||||
}
|
||||
|
||||
if (text.startsWith('/')) {
|
||||
return APP_BASE_URL + text;
|
||||
}
|
||||
|
||||
return `${APP_BASE_URL}/${text}`;
|
||||
}
|
||||
|
||||
function getStoredLastResult() {
|
||||
try {
|
||||
const saved = localStorage.getItem('asgard_last_result');
|
||||
|
|
@ -435,12 +477,12 @@ function goToLastResult(event) {
|
|||
const saved = getStoredLastResult();
|
||||
|
||||
if (saved && saved.url) {
|
||||
window.location.href = saved.url;
|
||||
window.location.href = normalizeAppUrl(saved.url);
|
||||
return;
|
||||
}
|
||||
|
||||
if (saved && saved.id) {
|
||||
window.location.href = `/student/result/${saved.id}`;
|
||||
window.location.href = makeResultUrl(saved.id);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -448,10 +490,9 @@ function goToLastResult(event) {
|
|||
window.location.href = routePrediction;
|
||||
}
|
||||
|
||||
// Bagian penting: menyimpan hasil terakhir agar setelah klik kembali tetap bisa dibuka dari navbar
|
||||
function saveLastRecommendationResult() {
|
||||
const resultId = @json($riwayat->id ?? '');
|
||||
const resultUrl = resultId ? `/student/result/${resultId}` : window.location.pathname;
|
||||
const resultUrl = resultId ? makeResultUrl(resultId) : window.location.href;
|
||||
|
||||
const resultData = {
|
||||
id: resultId,
|
||||
|
|
@ -477,7 +518,7 @@ function saveLastRecommendationResult() {
|
|||
|
||||
if (token) {
|
||||
try {
|
||||
await fetch('/api/logout', {
|
||||
await fetch(apiLogoutUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
|
|
@ -489,13 +530,11 @@ function saveLastRecommendationResult() {
|
|||
}
|
||||
}
|
||||
|
||||
localStorage.removeItem('access_token');
|
||||
localStorage.removeItem('user_role');
|
||||
clearLoginStorage();
|
||||
localStorage.removeItem('asgard_last_result');
|
||||
window.location.href = routeLogin;
|
||||
}
|
||||
|
||||
// bfcache/pageshow membuat hasil tetap tersimpan walaupun user memakai tombol back browser
|
||||
window.addEventListener('pageshow', function () {
|
||||
saveLastRecommendationResult();
|
||||
});
|
||||
|
|
@ -509,7 +548,7 @@ function saveLastRecommendationResult() {
|
|||
return;
|
||||
}
|
||||
|
||||
if (role !== 'siswa') {
|
||||
if (role !== 'siswa' && role !== 'student') {
|
||||
redirectByRole(role);
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,98 +6,34 @@
|
|||
<title>Cek Rekomendasi - ASGARD</title>
|
||||
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Plus Jakarta Sans', sans-serif;
|
||||
background:
|
||||
radial-gradient(circle at 8% 12%, rgba(16, 185, 129, 0.10), transparent 32%),
|
||||
radial-gradient(circle at 92% 8%, rgba(59, 130, 246, 0.08), transparent 30%),
|
||||
linear-gradient(180deg, #f8fafc 0%, #f3f7fb 50%, #f8fafc 100%);
|
||||
background: linear-gradient(180deg, #f8fafc 0%, #f3f7fb 50%, #f8fafc 100%);
|
||||
}
|
||||
|
||||
nav {
|
||||
box-shadow: 0 10px 35px rgba(15, 23, 42, 0.05);
|
||||
}
|
||||
|
||||
nav a {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
nav a::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: -10px;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
border-radius: 999px;
|
||||
background: #10b981;
|
||||
transition: width 0.25s ease;
|
||||
}
|
||||
|
||||
nav a:hover::after,
|
||||
nav a.text-emerald-600::after {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-shadow {
|
||||
box-shadow: 0 18px 50px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
main > div > div.bg-white {
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
backdrop-filter: blur(16px);
|
||||
box-shadow: 0 24px 70px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
main section {
|
||||
transition: transform 0.22s ease, box-shadow 0.22s ease, border-color 0.22s ease;
|
||||
}
|
||||
|
||||
main section:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 16px 35px rgba(15, 23, 42, 0.055);
|
||||
border-color: rgba(16, 185, 129, 0.18);
|
||||
}
|
||||
|
||||
input,
|
||||
select {
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease, background-color 0.2s ease;
|
||||
}
|
||||
|
||||
input:hover,
|
||||
select:hover {
|
||||
border-color: rgba(16, 185, 129, 0.45);
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
input:focus,
|
||||
select:focus {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
#alert-box {
|
||||
box-shadow: 0 14px 35px rgba(239, 68, 68, 0.08);
|
||||
}
|
||||
|
||||
footer {
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="flex flex-col min-h-screen text-slate-800">
|
||||
|
||||
<!-- Navbar -->
|
||||
<nav class="sticky top-0 z-50 bg-white/95 backdrop-blur-md border-b border-slate-100 px-6 py-4">
|
||||
<div class="max-w-7xl mx-auto flex items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-10 h-10 bg-emerald-100 rounded-xl flex items-center justify-center text-emerald-600 font-bold">
|
||||
AS
|
||||
</div>
|
||||
|
||||
<span class="font-black text-xl tracking-tighter text-emerald-950 uppercase italic">
|
||||
ASGARD.
|
||||
</span>
|
||||
|
|
@ -112,30 +48,16 @@
|
|||
Cek Rekomendasi
|
||||
</a>
|
||||
|
||||
<!-- Menu untuk membuka hasil terakhir -->
|
||||
<a href="#" onclick="goToLastResult(event)" class="hover:text-emerald-600 transition">
|
||||
Hasil Rekomendasi
|
||||
</a>
|
||||
|
||||
<!-- <a href="#" class="hover:text-emerald-600 transition">
|
||||
Kontak BK
|
||||
</a> -->
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="hidden sm:flex items-center gap-3 bg-emerald-50 px-4 py-2 rounded-full border border-emerald-100">
|
||||
<div class="w-2 h-2 bg-emerald-500 rounded-full animate-pulse"></div>
|
||||
<span class="text-[10px] font-bold text-emerald-700 uppercase">
|
||||
Siswa
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button onclick="handleLogout()" class="bg-red-50 hover:bg-red-100 text-red-600 border border-red-100 px-4 py-2 rounded-full transition-all">
|
||||
<span class="text-[10px] font-bold uppercase tracking-widest">
|
||||
Logout
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<button onclick="handleLogout()" class="bg-red-50 hover:bg-red-100 text-red-600 border border-red-100 px-4 py-2 rounded-full transition-all">
|
||||
<span class="text-[10px] font-bold uppercase tracking-widest">
|
||||
Logout
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
|
@ -153,7 +75,7 @@
|
|||
</h1>
|
||||
|
||||
<p class="text-slate-500 mt-3 text-sm md:text-base leading-relaxed">
|
||||
Isi data berikut jika ingin membuat rekomendasi baru. Jika ingin melihat hasil sebelumnya, klik menu <b>Hasil Rekomendasi</b> pada navbar.
|
||||
Isi data berikut untuk mendapatkan rekomendasi jurusan.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -161,7 +83,6 @@
|
|||
|
||||
<div class="space-y-6">
|
||||
|
||||
<!-- Kondisi ekonomi -->
|
||||
<section class="bg-slate-50 border border-slate-100 rounded-3xl p-5 md:p-6">
|
||||
<h2 class="font-extrabold text-slate-800 mb-1">
|
||||
💰 Kondisi Ekonomi
|
||||
|
|
@ -176,15 +97,9 @@
|
|||
id="gaji_ortu"
|
||||
min="0"
|
||||
placeholder="Contoh: 3000000"
|
||||
class="w-full rounded-2xl border border-slate-200 bg-white px-4 py-4 text-base font-semibold text-slate-700 outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100"
|
||||
>
|
||||
|
||||
<p class="text-xs text-slate-400 mt-2">
|
||||
Isi dengan angka saja tanpa titik atau koma.
|
||||
</p>
|
||||
class="w-full rounded-2xl border border-slate-200 bg-white px-4 py-4 text-base font-semibold text-slate-700 outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
|
||||
</section>
|
||||
|
||||
<!-- Minat dan bakat -->
|
||||
<section class="bg-emerald-50/40 border border-emerald-100 rounded-3xl p-5 md:p-6">
|
||||
<h2 class="font-extrabold text-emerald-800 mb-4">
|
||||
🎯 Kuesioner Minat dan Bakat
|
||||
|
|
@ -273,7 +188,6 @@ class="w-full rounded-2xl border border-slate-200 bg-white px-4 py-4 text-base f
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Nilai rapor -->
|
||||
<section class="bg-slate-50 border border-slate-100 rounded-3xl p-5 md:p-6">
|
||||
<h2 class="font-extrabold text-slate-700 mb-2">
|
||||
📚 Nilai Rapor
|
||||
|
|
@ -284,146 +198,57 @@ class="w-full rounded-2xl border border-slate-200 bg-white px-4 py-4 text-base f
|
|||
</p>
|
||||
|
||||
<div class="grid grid-cols-2 md:grid-cols-5 gap-4">
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-500 mb-2">Agama</label>
|
||||
<input type="number" id="agama" min="0" max="100" placeholder="0 - 100" class="w-full rounded-xl border border-slate-200 bg-white px-3 py-3 font-semibold outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
|
||||
</div>
|
||||
@php
|
||||
$mapel = [
|
||||
'agama' => 'Agama',
|
||||
'pkn' => 'PKN',
|
||||
'bahasa_indonesia' => 'B. Indonesia',
|
||||
'bahasa_inggris' => 'B. Inggris',
|
||||
'matematika' => 'Matematika',
|
||||
'fisika' => 'Fisika',
|
||||
'kimia' => 'Kimia',
|
||||
'biologi' => 'Biologi',
|
||||
'sejarah' => 'Sejarah',
|
||||
'geografi' => 'Geografi',
|
||||
'ekonomi_mapel' => 'Ekonomi',
|
||||
'sosiologi' => 'Sosiologi',
|
||||
'seni_budaya' => 'Seni Budaya',
|
||||
'pjok' => 'PJOK',
|
||||
'informatika' => 'Informatika',
|
||||
];
|
||||
@endphp
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-500 mb-2">PKN</label>
|
||||
<input type="number" id="pkn" min="0" max="100" placeholder="0 - 100" class="w-full rounded-xl border border-slate-200 bg-white px-3 py-3 font-semibold outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
|
||||
</div>
|
||||
@foreach ($mapel as $id => $label)
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-500 mb-2">
|
||||
{{ $label }}
|
||||
</label>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-500 mb-2">B. Indonesia</label>
|
||||
<input type="number" id="bahasa_indonesia" min="0" max="100" placeholder="0 - 100" class="w-full rounded-xl border border-slate-200 bg-white px-3 py-3 font-semibold outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-500 mb-2">B. Inggris</label>
|
||||
<input type="number" id="bahasa_inggris" min="0" max="100" placeholder="0 - 100" class="w-full rounded-xl border border-slate-200 bg-white px-3 py-3 font-semibold outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-500 mb-2">Matematika</label>
|
||||
<input type="number" id="matematika" min="0" max="100" placeholder="0 - 100" class="w-full rounded-xl border border-slate-200 bg-white px-3 py-3 font-semibold outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-500 mb-2">Fisika</label>
|
||||
<input type="number" id="fisika" min="0" max="100" placeholder="0 - 100" class="w-full rounded-xl border border-slate-200 bg-white px-3 py-3 font-semibold outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-500 mb-2">Kimia</label>
|
||||
<input type="number" id="kimia" min="0" max="100" placeholder="0 - 100" class="w-full rounded-xl border border-slate-200 bg-white px-3 py-3 font-semibold outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-500 mb-2">Biologi</label>
|
||||
<input type="number" id="biologi" min="0" max="100" placeholder="0 - 100" class="w-full rounded-xl border border-slate-200 bg-white px-3 py-3 font-semibold outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-500 mb-2">Sejarah</label>
|
||||
<input type="number" id="sejarah" min="0" max="100" placeholder="0 - 100" class="w-full rounded-xl border border-slate-200 bg-white px-3 py-3 font-semibold outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-500 mb-2">Geografi</label>
|
||||
<input type="number" id="geografi" min="0" max="100" placeholder="0 - 100" class="w-full rounded-xl border border-slate-200 bg-white px-3 py-3 font-semibold outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-500 mb-2">Ekonomi</label>
|
||||
<input type="number" id="ekonomi_mapel" min="0" max="100" placeholder="0 - 100" class="w-full rounded-xl border border-slate-200 bg-white px-3 py-3 font-semibold outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-500 mb-2">Sosiologi</label>
|
||||
<input type="number" id="sosiologi" min="0" max="100" placeholder="0 - 100" class="w-full rounded-xl border border-slate-200 bg-white px-3 py-3 font-semibold outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-500 mb-2">Seni Budaya</label>
|
||||
<input type="number" id="seni_budaya" min="0" max="100" placeholder="0 - 100" class="w-full rounded-xl border border-slate-200 bg-white px-3 py-3 font-semibold outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-500 mb-2">PJOK</label>
|
||||
<input type="number" id="pjok" min="0" max="100" placeholder="0 - 100" class="w-full rounded-xl border border-slate-200 bg-white px-3 py-3 font-semibold outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-500 mb-2">Informatika</label>
|
||||
<input type="number" id="informatika" min="0" max="100" placeholder="0 - 100" class="w-full rounded-xl border border-slate-200 bg-white px-3 py-3 font-semibold outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
|
||||
</div>
|
||||
<input
|
||||
type="number"
|
||||
id="{{ $id }}"
|
||||
min="0"
|
||||
max="100"
|
||||
placeholder="0 - 100"
|
||||
class="w-full rounded-xl border border-slate-200 bg-white px-3 py-3 font-semibold outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100">
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Tombol -->
|
||||
<div class="flex flex-col sm:flex-row gap-3 pt-2">
|
||||
<a href="{{ route('student.profile') }}"
|
||||
class="w-full sm:w-1/3 text-center bg-slate-100 hover:bg-slate-200 text-slate-600 font-bold py-4 rounded-2xl transition">
|
||||
Kembali
|
||||
</a>
|
||||
|
||||
<button id="btn-submit"
|
||||
class="w-full sm:w-2/3 bg-emerald-600 hover:bg-emerald-700 text-white font-bold py-4 rounded-2xl transition shadow-lg shadow-emerald-200">
|
||||
Cek Rekomendasi
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
id="btnSubmit"
|
||||
class="w-full bg-emerald-600 hover:bg-emerald-700 text-white font-extrabold py-4 px-6 rounded-2xl transition">
|
||||
Proses Rekomendasi
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="bg-[#062C22] text-white py-16 px-6 mt-16">
|
||||
<div class="max-w-7xl mx-auto grid grid-cols-1 md:grid-cols-3 gap-12">
|
||||
<div class="space-y-6">
|
||||
<span class="font-black text-2xl tracking-tighter uppercase italic">
|
||||
ASGARD.
|
||||
</span>
|
||||
|
||||
<p class="text-slate-300 text-sm leading-relaxed max-w-xs">
|
||||
Sistem rekomendasi jurusan kuliah SMAN 4 Jember berbasis profil siswa dan data akademik.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h5 class="font-bold text-xs uppercase tracking-widest text-emerald-400 mb-6">
|
||||
Informasi
|
||||
</h5>
|
||||
|
||||
<ul class="space-y-4 text-sm text-slate-300">
|
||||
<li>
|
||||
<a href="{{ route('panduan') }}" class="hover:text-white transition">
|
||||
Panduan Sistem
|
||||
</a>
|
||||
</li>
|
||||
<!-- <li>
|
||||
<a href="#" class="hover:text-white transition">
|
||||
Kebijakan Privasi
|
||||
</a>
|
||||
</li> -->
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h5 class="font-bold text-xs uppercase tracking-widest text-emerald-400 mb-6">
|
||||
Sekolah
|
||||
</h5>
|
||||
|
||||
<ul class="space-y-4 text-sm text-slate-300">
|
||||
<li>SMAN 4 Jember</li>
|
||||
<li>Jl. Wolter Monginsidi No.24</li>
|
||||
<li>Jember, Jawa Timur</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-w-7xl mx-auto mt-16 pt-8 border-t border-white/10 text-center">
|
||||
<footer class="bg-[#062C22] text-white py-10 px-6 mt-10">
|
||||
<div class="max-w-7xl mx-auto text-center">
|
||||
<p class="text-[10px] font-medium uppercase tracking-[0.35em] text-slate-400">
|
||||
© 2026 GALANG SEFIAN ADJI - POLITEKNIK NEGERI JEMBER
|
||||
</p>
|
||||
|
|
@ -431,39 +256,60 @@ class="w-full sm:w-2/3 bg-emerald-600 hover:bg-emerald-700 text-white font-bold
|
|||
</footer>
|
||||
|
||||
<script>
|
||||
function getAppBaseUrl() {
|
||||
const origin = window.location.origin;
|
||||
const segments = window.location.pathname.split('/').filter(Boolean);
|
||||
const studentIndex = segments.indexOf('student');
|
||||
|
||||
if (studentIndex > 0) {
|
||||
return origin + '/' + segments.slice(0, studentIndex).join('/');
|
||||
}
|
||||
|
||||
return origin;
|
||||
}
|
||||
|
||||
const APP_BASE_URL = getAppBaseUrl();
|
||||
|
||||
const routeLogin = `${APP_BASE_URL}/login`;
|
||||
const routeDashboard = `${APP_BASE_URL}/student/profile`;
|
||||
const routePrediction = `${APP_BASE_URL}/student/prediction`;
|
||||
const routeAdminDashboard = `${APP_BASE_URL}/admin/dashboard`;
|
||||
|
||||
const apiLogoutUrl = `${APP_BASE_URL}/api/logout`;
|
||||
const apiProsesRekomendasiUrl = `${APP_BASE_URL}/api/student/proses-rekomendasi`;
|
||||
const studentResultUrlTemplate = `${APP_BASE_URL}/student/result/__ID__`;
|
||||
|
||||
const btnSubmit = document.getElementById('btnSubmit');
|
||||
const alertBox = document.getElementById('alert-box');
|
||||
const btnSubmit = document.getElementById('btn-submit');
|
||||
|
||||
const routeLogin = @json(route('login'));
|
||||
const routeDashboard = @json(route('student.profile'));
|
||||
const routePrediction = @json(route('student.prediction'));
|
||||
const routeAdminDashboard = @json(route('admin.dashboard'));
|
||||
|
||||
function showError(message) {
|
||||
alertBox.classList.remove('hidden');
|
||||
alertBox.textContent = message;
|
||||
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
|
||||
function hideError() {
|
||||
alertBox.classList.add('hidden');
|
||||
alertBox.textContent = '';
|
||||
}
|
||||
|
||||
function getNumberValue(id) {
|
||||
return Number(document.getElementById(id).value || 0);
|
||||
}
|
||||
|
||||
function getAccessToken() {
|
||||
return localStorage.getItem('access_token');
|
||||
return localStorage.getItem('access_token') ||
|
||||
localStorage.getItem('token') ||
|
||||
sessionStorage.getItem('access_token') ||
|
||||
sessionStorage.getItem('token') ||
|
||||
'';
|
||||
}
|
||||
|
||||
function getUserRole() {
|
||||
return localStorage.getItem('user_role');
|
||||
return localStorage.getItem('user_role') ||
|
||||
localStorage.getItem('role') ||
|
||||
sessionStorage.getItem('user_role') ||
|
||||
sessionStorage.getItem('role') ||
|
||||
'';
|
||||
}
|
||||
|
||||
function clearLoginStorage() {
|
||||
localStorage.removeItem('access_token');
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user_role');
|
||||
localStorage.removeItem('role');
|
||||
localStorage.removeItem('user_name');
|
||||
localStorage.removeItem('name');
|
||||
|
||||
sessionStorage.removeItem('access_token');
|
||||
sessionStorage.removeItem('token');
|
||||
sessionStorage.removeItem('user_role');
|
||||
sessionStorage.removeItem('role');
|
||||
}
|
||||
|
||||
function redirectByRole(role) {
|
||||
|
|
@ -472,7 +318,7 @@ function redirectByRole(role) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (role === 'siswa') {
|
||||
if (role === 'siswa' || role === 'student') {
|
||||
window.location.href = routeDashboard;
|
||||
return;
|
||||
}
|
||||
|
|
@ -480,6 +326,41 @@ function redirectByRole(role) {
|
|||
window.location.href = routeLogin;
|
||||
}
|
||||
|
||||
function makeResultUrl(id) {
|
||||
return id ? studentResultUrlTemplate.replace('__ID__', encodeURIComponent(id)) : '';
|
||||
}
|
||||
|
||||
function normalizeAppUrl(url) {
|
||||
if (!url) return '';
|
||||
|
||||
const text = String(url);
|
||||
|
||||
if (text.startsWith('http://') || text.startsWith('https://')) {
|
||||
return text;
|
||||
}
|
||||
|
||||
if (text.startsWith('/')) {
|
||||
return APP_BASE_URL + text;
|
||||
}
|
||||
|
||||
return `${APP_BASE_URL}/${text}`;
|
||||
}
|
||||
|
||||
async function parseJsonResponse(response) {
|
||||
const text = await response.text();
|
||||
|
||||
try {
|
||||
return text ? JSON.parse(text) : {};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: text && text.trim().startsWith('<')
|
||||
? 'Server mengembalikan halaman HTML, bukan JSON. Biasanya URL API salah, route API belum terdaftar, atau Flask API belum aktif.'
|
||||
: (text || response.statusText || 'Respon server tidak valid.')
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getStoredLastResult() {
|
||||
try {
|
||||
const saved = localStorage.getItem('asgard_last_result');
|
||||
|
|
@ -495,12 +376,12 @@ function goToLastResult(event) {
|
|||
const saved = getStoredLastResult();
|
||||
|
||||
if (saved && saved.url) {
|
||||
window.location.href = saved.url;
|
||||
window.location.href = normalizeAppUrl(saved.url);
|
||||
return;
|
||||
}
|
||||
|
||||
if (saved && saved.id) {
|
||||
window.location.href = `/student/result/${saved.id}`;
|
||||
window.location.href = makeResultUrl(saved.id);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -508,44 +389,41 @@ function goToLastResult(event) {
|
|||
window.location.href = routePrediction;
|
||||
}
|
||||
|
||||
function validateForm(payload) {
|
||||
const scoreFields = [
|
||||
'agama', 'pkn', 'bahasa_indonesia', 'bahasa_inggris', 'matematika',
|
||||
'fisika', 'kimia', 'biologi', 'sejarah', 'geografi',
|
||||
'ekonomi_mapel', 'sosiologi', 'seni_budaya', 'pjok', 'informatika'
|
||||
];
|
||||
async function handleLogout() {
|
||||
const token = getAccessToken();
|
||||
|
||||
for (const field of scoreFields) {
|
||||
const rawValue = document.getElementById(field).value;
|
||||
|
||||
if (rawValue === '') {
|
||||
return `Nilai ${field.replaceAll('_', ' ')} wajib diisi.`;
|
||||
}
|
||||
|
||||
const value = Number(payload[field]);
|
||||
|
||||
if (isNaN(value) || value < 0 || value > 100) {
|
||||
return `Nilai ${field.replaceAll('_', ' ')} harus berada pada rentang 0 sampai 100.`;
|
||||
if (token) {
|
||||
try {
|
||||
await fetch(apiLogoutUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
if (document.getElementById('gaji_ortu').value === '') {
|
||||
return 'Pendapatan gabungan orang tua wajib diisi.';
|
||||
}
|
||||
clearLoginStorage();
|
||||
localStorage.removeItem('asgard_last_result');
|
||||
window.location.href = routeLogin;
|
||||
}
|
||||
|
||||
if (Number(payload.gaji_ortu) < 0) {
|
||||
return 'Pendapatan gabungan orang tua tidak boleh bernilai negatif.';
|
||||
}
|
||||
function showError(message) {
|
||||
alertBox.innerText = message;
|
||||
alertBox.classList.remove('hidden');
|
||||
}
|
||||
|
||||
const questionIds = ['q1', 'q2', 'q3', 'q4', 'q5'];
|
||||
function hideError() {
|
||||
alertBox.innerText = '';
|
||||
alertBox.classList.add('hidden');
|
||||
}
|
||||
|
||||
for (const id of questionIds) {
|
||||
if (!document.getElementById(id).value) {
|
||||
return 'Semua pertanyaan minat dan bakat wajib diisi.';
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
function getNumberValue(id) {
|
||||
const value = document.getElementById(id).value;
|
||||
return value === '' ? null : Number(value);
|
||||
}
|
||||
|
||||
function calculateRiasecScores() {
|
||||
|
|
@ -558,12 +436,10 @@ function calculateRiasecScores() {
|
|||
c: 0
|
||||
};
|
||||
|
||||
const questionIds = ['q1', 'q2', 'q3', 'q4', 'q5'];
|
||||
|
||||
questionIds.forEach(id => {
|
||||
['q1', 'q2', 'q3', 'q4', 'q5'].forEach(id => {
|
||||
const value = document.getElementById(id).value;
|
||||
|
||||
if (value && scores.hasOwnProperty(value)) {
|
||||
if (value && Object.prototype.hasOwnProperty.call(scores, value)) {
|
||||
scores[value] += 20;
|
||||
}
|
||||
});
|
||||
|
|
@ -571,27 +447,54 @@ function calculateRiasecScores() {
|
|||
return scores;
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
const token = getAccessToken();
|
||||
function validateForm(payload) {
|
||||
const scoreFields = [
|
||||
'agama',
|
||||
'pkn',
|
||||
'bahasa_indonesia',
|
||||
'bahasa_inggris',
|
||||
'matematika',
|
||||
'fisika',
|
||||
'kimia',
|
||||
'biologi',
|
||||
'sejarah',
|
||||
'geografi',
|
||||
'ekonomi_mapel',
|
||||
'sosiologi',
|
||||
'seni_budaya',
|
||||
'pjok',
|
||||
'informatika'
|
||||
];
|
||||
|
||||
if (token) {
|
||||
try {
|
||||
await fetch('/api/logout', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error);
|
||||
for (const field of scoreFields) {
|
||||
const rawValue = document.getElementById(field).value;
|
||||
|
||||
if (rawValue === '') {
|
||||
return `Nilai ${field.replaceAll('_', ' ')} wajib diisi.`;
|
||||
}
|
||||
|
||||
const value = Number(payload[field]);
|
||||
|
||||
if (Number.isNaN(value) || value < 0 || value > 100) {
|
||||
return `Nilai ${field.replaceAll('_', ' ')} harus berada pada rentang 0 sampai 100.`;
|
||||
}
|
||||
}
|
||||
|
||||
localStorage.removeItem('access_token');
|
||||
localStorage.removeItem('user_role');
|
||||
localStorage.removeItem('asgard_last_result');
|
||||
window.location.href = routeLogin;
|
||||
if (document.getElementById('gaji_ortu').value === '') {
|
||||
return 'Pendapatan gabungan orang tua wajib diisi.';
|
||||
}
|
||||
|
||||
if (Number(payload.gaji_ortu) < 0) {
|
||||
return 'Pendapatan gabungan orang tua tidak boleh bernilai negatif.';
|
||||
}
|
||||
|
||||
for (const id of ['q1', 'q2', 'q3', 'q4', 'q5']) {
|
||||
if (!document.getElementById(id).value) {
|
||||
return 'Semua pertanyaan minat dan bakat wajib diisi.';
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
|
|
@ -603,9 +506,8 @@ function calculateRiasecScores() {
|
|||
return;
|
||||
}
|
||||
|
||||
if (role !== 'siswa') {
|
||||
if (role !== 'siswa' && role !== 'student') {
|
||||
redirectByRole(role);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -625,12 +527,12 @@ function calculateRiasecScores() {
|
|||
return;
|
||||
}
|
||||
|
||||
if (role !== 'siswa') {
|
||||
if (role !== 'siswa' && role !== 'student') {
|
||||
redirectByRole(role);
|
||||
return;
|
||||
}
|
||||
|
||||
btnSubmit.innerHTML = 'Memproses Rekomendasi...';
|
||||
btnSubmit.innerText = 'Memproses Rekomendasi...';
|
||||
btnSubmit.disabled = true;
|
||||
|
||||
const scores = calculateRiasecScores();
|
||||
|
|
@ -651,12 +553,14 @@ function calculateRiasecScores() {
|
|||
seni_budaya: getNumberValue('seni_budaya'),
|
||||
pjok: getNumberValue('pjok'),
|
||||
informatika: getNumberValue('informatika'),
|
||||
|
||||
riasec_r: scores.r,
|
||||
riasec_i: scores.i,
|
||||
riasec_a: scores.a,
|
||||
riasec_s: scores.s,
|
||||
riasec_e: scores.e,
|
||||
riasec_c: scores.c,
|
||||
|
||||
gaji_ortu: getNumberValue('gaji_ortu')
|
||||
};
|
||||
|
||||
|
|
@ -664,34 +568,27 @@ function calculateRiasecScores() {
|
|||
|
||||
if (validationError) {
|
||||
showError(validationError);
|
||||
btnSubmit.innerHTML = 'Proses Rekomendasi';
|
||||
btnSubmit.innerText = 'Proses Rekomendasi';
|
||||
btnSubmit.disabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/student/proses-rekomendasi', {
|
||||
const response = await fetch(apiProsesRekomendasiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
const result = await parseJsonResponse(response);
|
||||
|
||||
if (response.ok && result.success) {
|
||||
window.location.href = result.redirect_url;
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.status === 401) {
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
clearLoginStorage();
|
||||
showError('Sesi login berakhir. Silakan login kembali.');
|
||||
localStorage.removeItem('access_token');
|
||||
localStorage.removeItem('user_role');
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.href = routeLogin;
|
||||
|
|
@ -700,17 +597,31 @@ function calculateRiasecScores() {
|
|||
return;
|
||||
}
|
||||
|
||||
if (response.status === 403) {
|
||||
showError(result.message || 'Anda tidak memiliki akses ke halaman ini.');
|
||||
if (response.ok && result.success) {
|
||||
const riwayatId = result.riwayat_id || result.id || result.data?.id || '';
|
||||
const prediksiJurusan = result.prediksi_jurusan || result.hasil_rekomendasi_jurusan || result.data?.hasil_rekomendasi_jurusan || '';
|
||||
const redirectUrl = normalizeAppUrl(result.redirect_url || makeResultUrl(riwayatId));
|
||||
|
||||
if (riwayatId && prediksiJurusan) {
|
||||
localStorage.setItem('asgard_last_result', JSON.stringify({
|
||||
id: riwayatId,
|
||||
jurusan: prediksiJurusan,
|
||||
url: redirectUrl,
|
||||
tanggal: new Date().toLocaleString('id-ID')
|
||||
}));
|
||||
}
|
||||
|
||||
window.location.href = redirectUrl || routeDashboard;
|
||||
return;
|
||||
}
|
||||
|
||||
showError(result.message || 'Terjadi kesalahan saat memproses rekomendasi.');
|
||||
|
||||
} catch (error) {
|
||||
showError('Kesalahan jaringan. Pastikan Laravel dan API Flask sedang berjalan.');
|
||||
console.error(error);
|
||||
showError(error.message || 'Kesalahan jaringan. Pastikan koneksi server dan Flask API PythonAnywhere aktif.');
|
||||
} finally {
|
||||
btnSubmit.innerHTML = 'Proses Rekomendasi';
|
||||
btnSubmit.innerText = 'Proses Rekomendasi';
|
||||
btnSubmit.disabled = false;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,25 +12,75 @@
|
|||
use App\Http\Controllers\Api\Student\DashboardStudentController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::post('/login', [AuthController::class, 'login']);
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| API Authentication & Public Routes
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
Route::post('/login', [AuthController::class, 'login'])->name('api.login');
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Test Koneksi Laravel ke Flask PythonAnywhere
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
Route::get('/python/health', [RecommendationApiController::class, 'health'])
|
||||
->name('api.python.health');
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Protected API Routes (Membutuhkan Login / Token Sanctum)
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
Route::middleware('auth:sanctum')->group(function () {
|
||||
Route::middleware('role:admin')->prefix('admin')->group(function () {
|
||||
Route::apiResource('siswa', SiswaController::class);
|
||||
Route::apiResource('jurusan', JurusanController::class);
|
||||
Route::apiResource('guru', GuruController::class);
|
||||
Route::apiResource('alumni', AlumniController::class);
|
||||
Route::apiResource('prospek-kerja', ProspekKerjaController::class);
|
||||
Route::get('/dashboard-stats', [DashboardAdminController::class, 'index']);
|
||||
Route::get('/riwayat', [RiwayatController::class, 'index']);
|
||||
Route::get('/riwayat/{id}', [RiwayatController::class, 'show']);
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Admin Routes Group
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
Route::middleware('role:admin')->prefix('admin')->name('api.admin.')->group(function () {
|
||||
// Dashboard Stats
|
||||
Route::get('/dashboard-stats', [DashboardAdminController::class, 'index'])->name('dashboard-stats');
|
||||
|
||||
// CRUD Master Data Siswa
|
||||
Route::apiResource('/siswa', SiswaController::class);
|
||||
|
||||
// CRUD Master Data Lainnya
|
||||
Route::apiResource('/jurusan', JurusanController::class);
|
||||
Route::apiResource('/guru', GuruController::class);
|
||||
Route::apiResource('/alumni', AlumniController::class);
|
||||
Route::apiResource('/riwayat', RiwayatController::class);
|
||||
Route::apiResource('/prospek-kerja', ProspekKerjaController::class);
|
||||
});
|
||||
|
||||
Route::middleware('role:siswa')->prefix('student')->group(function () {
|
||||
Route::get('/dashboard-stats', [DashboardStudentController::class, 'index']);
|
||||
Route::post('/proses-rekomendasi', [RecommendationApiController::class, 'hitungPrediksi']);
|
||||
Route::get('/hasil-rekomendasi/{id}', [RecommendationApiController::class, 'getHasilRekomendasi']);
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Student (Siswa) Routes Group
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
Route::middleware('role:siswa')->prefix('student')->name('api.student.')->group(function () {
|
||||
Route::get('/dashboard-stats', [DashboardStudentController::class, 'index'])
|
||||
->name('dashboard-stats');
|
||||
|
||||
Route::post('/proses-rekomendasi', [RecommendationApiController::class, 'hitungPrediksi'])
|
||||
->name('proses-rekomendasi');
|
||||
|
||||
Route::get('/hasil-rekomendasi/{id}', [RecommendationApiController::class, 'getHasilRekomendasi'])
|
||||
->name('hasil-rekomendasi');
|
||||
|
||||
Route::post('/recommendation/predict', [RecommendationApiController::class, 'hitungPrediksi'])
|
||||
->name('recommendation.predict');
|
||||
|
||||
Route::get('/recommendation/result/{id}', [RecommendationApiController::class, 'getHasilRekomendasi'])
|
||||
->name('recommendation.result');
|
||||
});
|
||||
|
||||
Route::post('/logout', [AuthController::class, 'logout']);
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Logout
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
Route::post('/logout', [AuthController::class, 'logout'])->name('api.logout');
|
||||
});
|
||||
|
|
@ -29,14 +29,16 @@
|
|||
Route::prefix('admin')->name('admin.')->group(function () {
|
||||
Route::get('/dashboard', [AdminDashboardController::class, 'index'])->name('dashboard');
|
||||
|
||||
// PERBAIKAN: Format names menggunakan string tunggal 'siswa' seperti milik prospek-kerja
|
||||
Route::resource('siswa', SiswaController::class)->only(['index', 'create', 'edit'])->names(['index' => 'siswa']);
|
||||
|
||||
Route::resource('alumni', AlumniController::class)->only(['index', 'create', 'edit'])->names(['index' => 'alumni']);
|
||||
Route::resource('jurusan', JurusanController::class)->only(['index', 'create', 'edit'])->names(['index' => 'jurusan']);
|
||||
Route::resource('guru', GuruController::class)->only(['index', 'create', 'edit'])->names(['index' => 'guru']);
|
||||
Route::resource('prospek-kerja', ProspekKerjaController::class)->only(['index', 'create', 'edit'])->names(['index' => 'prospek-kerja']);
|
||||
Route::get('/riwayat', [RiwayatController::class, 'index'])->name('riwayat');
|
||||
|
||||
Route::get('/latih-model', function () {
|
||||
return view('admin.latihmodel');
|
||||
Route::get('/riwayat', [RiwayatController::class, 'index'])->name('riwayat');
|
||||
Route::get('/latih-model', function() {
|
||||
// Logika latih model Anda
|
||||
})->name('latih-model');
|
||||
});
|
||||
Loading…
Reference in New Issue