perbaikan

bismillah tidak ada error
This commit is contained in:
Ryfandii 2026-06-17 08:08:25 +07:00
parent e4cae10aa5
commit d5eedb16ae
23 changed files with 1376 additions and 726 deletions

View File

@ -100,7 +100,9 @@ public function sendOtp(Request $request)
// Simpan email ke session untuk prefill form login // Simpan email ke session untuk prefill form login
session(['email' => $user->email]); session(['email' => $user->email]);
return back()->with('otp_sent', 'OTP berhasil dikirim!'); return back()
->with('otp_sent', 'OTP berhasil dikirim!')
->with('need_otp', true); // ← tambahkan ini
} }
@ -108,82 +110,116 @@ public function sendOtp(Request $request)
// Dipanggil dari form Langkah 2 di halaman login // Dipanggil dari form Langkah 2 di halaman login
public function loginOtp(Request $request) public function loginOtp(Request $request)
{ {
\Log::info('=== LOGIN OTP CALLED - AuthController ===');
$request->validate([ $request->validate([
'email' => 'required|email', 'email' => 'required|email',
'password' => 'required', 'password' => 'required',
'otp' => 'nullable|digits:6', 'otp' => 'nullable|digits:6',
]); ]);
// ✅ FIX: tambah with('guru') agar relasi ter-load $user = User::with(['guru', 'siswa'])->where('email', $request->email)->first();
$user = User::with('guru')->where('email', $request->email)->first();
if (!$user) { if (!$user) {
return back()->with('error', 'Email tidak ditemukan.'); return back()->with('error', 'Email tidak ditemukan.');
} }
// Cek password
if (!Hash::check($request->password, $user->password)) { if (!Hash::check($request->password, $user->password)) {
return back()->with('error', 'Password salah.'); return back()->with('error', 'Password salah.');
} }
// ================================================= // ADMIN → langsung login tanpa OTP
// ADMIN → LANGSUNG LOGIN TANPA OTP
// =================================================
if ($user->role === 'admin') { if ($user->role === 'admin') {
Auth::login($user); Auth::login($user);
$request->session()->regenerate(); $request->session()->regenerate();
return redirect()->route('admin.dashboard'); return redirect()->route('admin.dashboard');
} }
// ================================================= // Cek status nonaktif
// GURU / SISWA WAJIB OTP if ($user->role === 'guru' && (!$user->guru || $user->guru->status === 'nonaktif')) {
// ================================================= return back()->with('error', 'Akun Anda telah dinonaktifkan.');
if (in_array($user->role, ['guru', 'siswa'])) { }
if ($user->role === 'siswa' && (!$user->siswa || $user->siswa->status === 'nonaktif')) {
return back()->with('error', 'Akun Anda telah dinonaktifkan.');
}
// ✅ FIX: cek status nonaktif sebelum proses OTP // ── Cek cookie trusted device (per user per device) ──
if ($user->role === 'guru') { $cookieKey = 'trusted_device_' . $user->id;
$user->load('guru'); // reload fresh dari DB // SESUDAH
if (!$user->guru || $user->guru->status === 'nonaktif') { $cookieValue = \Illuminate\Support\Facades\Cookie::get($cookieKey);
return back()->with('error', 'Akun Anda telah dinonaktifkan. Hubungi administrator.'); $trustedDevices = $user->trusted_devices ?? [];
} $isDeviceTrusted = $cookieValue && in_array($cookieValue, $trustedDevices);
}
// DEBUG SEMENTARA — hapus setelah fix
\Log::info('COOKIE CHECK', [
'cookie_key' => $cookieKey,
'cookie_value' => $cookieValue,
'trusted_list' => $trustedDevices,
'is_trusted' => $isDeviceTrusted,
]);
// Device BARU → wajib OTP
if (!$isDeviceTrusted) {
if (empty($request->otp)) { if (empty($request->otp)) {
return back()->with('error', 'OTP wajib diisi.'); return back()
->withInput()
->with('error', 'Perangkat baru terdeteksi! Silakan minta OTP terlebih dahulu.')
->with('need_otp', true);
} }
if (!$user->otp) { if (!$user->otp) {
return back()->with('error', 'OTP belum diminta.'); return back()->withInput()
->with('error', 'OTP belum diminta.')
->with('need_otp', true);
} }
if ((string)$request->otp !== (string)$user->otp) { if ((string)$request->otp !== (string)$user->otp) {
return back()->with('error', 'Kode OTP salah.'); return back()->withInput()
->with('error', 'Kode OTP salah.')
->with('need_otp', true);
} }
if (!$user->otp_expired_at || now()->gt($user->otp_expired_at)) { if (!$user->otp_expired_at || now()->gt($user->otp_expired_at)) {
return back()->with('error', 'OTP sudah kadaluarsa.'); return back()->withInput()
->with('error', 'OTP sudah kadaluarsa.')
->with('need_otp', true);
} }
// Login // Generate token unik untuk device ini
$deviceToken = \Illuminate\Support\Str::random(60);
// Simpan ke trusted_devices (maks 10)
$trustedDevices[] = $deviceToken;
if (count($trustedDevices) > 10) array_shift($trustedDevices);
$user->update([
'otp' => null,
'otp_expired_at' => null,
'trusted_devices' => $trustedDevices,
]);
Auth::login($user); Auth::login($user);
$request->session()->regenerate(); $request->session()->regenerate();
// Hapus OTP // Simpan cookie selama 365 hari
$user->update([ $cookie = cookie($cookieKey, $deviceToken, 60 * 24 * 365, '/', null, false, true);
'otp' => null,
'otp_expired_at' => null,
]);
return match ($user->role) { return match ($user->role) {
'guru' => redirect()->route('guru.dashboard'), 'guru' => redirect()->route('guru.dashboard')->withCookie($cookie),
'siswa' => redirect()->route('siswa.dashboard'), 'siswa' => redirect()->route('siswa.dashboard')->withCookie($cookie),
default => redirect('/'), default => redirect('/')->withCookie($cookie),
}; };
} }
return back()->with('error', 'Role tidak dikenali.'); // Device LAMA → langsung login
Auth::login($user);
$request->session()->regenerate();
return match ($user->role) {
'guru' => redirect()->route('guru.dashboard'),
'siswa' => redirect()->route('siswa.dashboard'),
default => redirect('/'),
};
} }

View File

@ -33,6 +33,15 @@ public function create()
// ================= STORE ================= // ================= STORE =================
public function store(Request $request) public function store(Request $request)
{ {
if ($request->has('telepon')) {
$telepon = $request->telepon;
$telepon = ltrim($telepon, '+');
if (strpos($telepon, '62') === 0) {
$telepon = '0' . substr($telepon, 2);
}
$request->merge(['telepon' => $telepon]);
}
$request->validate([ $request->validate([
'nama' => 'required', 'nama' => 'required',
'nip' => 'required|unique:gurus,nip', 'nip' => 'required|unique:gurus,nip',
@ -42,7 +51,7 @@ public function store(Request $request)
'email' => 'required|email|unique:users,email' 'email' => 'required|email|unique:users,email'
]); ]);
// 🔥 CEK JIKA SUDAH ADA (ANTI DOUBLE INSERT) // 🔥 CEK JIKA SUDAH ADA (ANTI DOUBLE INSERT)
if (Guru::where('nip', $request->nip)->exists()) { if (Guru::where('nip', $request->nip)->exists()) {
return back()->withErrors(['nip' => 'NIP sudah ada'])->withInput(); return back()->withErrors(['nip' => 'NIP sudah ada'])->withInput();
} }
@ -83,6 +92,15 @@ public function edit($id)
// ================= UPDATE ================= // ================= UPDATE =================
public function update(Request $request, Guru $guru) public function update(Request $request, Guru $guru)
{ {
if ($request->has('telepon')) {
$telepon = $request->telepon;
$telepon = ltrim($telepon, '+');
if (strpos($telepon, '62') === 0) {
$telepon = '0' . substr($telepon, 2);
}
$request->merge(['telepon' => $telepon]);
}
$request->validate([ $request->validate([
'nama' => 'required', 'nama' => 'required',
'nip' => 'required|unique:gurus,nip,' . $guru->id, 'nip' => 'required|unique:gurus,nip,' . $guru->id,
@ -94,18 +112,18 @@ public function update(Request $request, Guru $guru)
$guru->update([ $guru->update([
'nama' => $request->nama, 'nama' => $request->nama,
'nip' => $request->nip, 'nip' => $request->nip,
'mapel_id' => $request->mapel_id, // 🔥 INI WAJIB 'mapel_id' => $request->mapel_id, // 🔥 INI WAJIB
'alamat' => $request->alamat, 'alamat' => $request->alamat,
'telepon' => $request->telepon, 'telepon' => $request->telepon,
]); ]);
// 🔥 UPDATE USER (TELEPON JUGA UPDATE) // 🔥 UPDATE USER (TELEPON JUGA UPDATE)
$user = User::where('guru_id', $guru->id)->first(); $user = User::where('guru_id', $guru->id)->first();
if ($user) { if ($user) {
$user->update([ $user->update([
'name' => $request->nama, 'name' => $request->nama,
'mapel_id' => $request->mapel_id, 'mapel_id' => $request->mapel_id,
'telepon' => $request->telepon // WAJIB ADA 'telepon' => $request->telepon // ✅ WAJIB ADA
]); ]);
} }

View File

@ -54,6 +54,11 @@ public function store(Request $request)
'jam_selesai' => 'required', 'jam_selesai' => 'required',
]); ]);
// ✅ Validasi jam selesai harus lebih besar dari jam mulai
if ($request->jam_selesai <= $request->jam_mulai) {
return back()->withInput()->with('error', 'Jam selesai harus lebih besar dari jam mulai!');
}
// 🔥 CEK BENTROK KELAS // 🔥 CEK BENTROK KELAS
$cekKelas = Jadwal::where('kelas_id', $request->kelas_id) $cekKelas = Jadwal::where('kelas_id', $request->kelas_id)
->where('hari', $request->hari) ->where('hari', $request->hari)
@ -120,6 +125,11 @@ public function update(Request $request, $id)
'jam_selesai' => 'required', 'jam_selesai' => 'required',
]); ]);
// ✅ Validasi jam selesai harus lebih besar dari jam mulai
if ($request->jam_selesai <= $request->jam_mulai) {
return back()->withInput()->with('error', 'Jam selesai harus lebih besar dari jam mulai!');
}
$jadwal->update([ $jadwal->update([
'kelas_id' => $request->kelas_id, 'kelas_id' => $request->kelas_id,
'mata_pelajaran_id' => $request->mata_pelajaran_id, 'mata_pelajaran_id' => $request->mata_pelajaran_id,

View File

@ -13,29 +13,50 @@ public function index()
return view('admin.profile.index'); return view('admin.profile.index');
} }
public function update(Request $request) public function update(Request $request)
{ {
$user = Auth::user(); $user = Auth::user();
$request->validate([ $rules = [
'name' => 'required', 'name' => 'required',
'photo' => 'nullable|image|mimes:jpg,jpeg,png|max:2048' 'photo' => 'nullable|image|mimes:jpg,jpeg,png,webp|max:2048',
]); ];
// update nama // Tambahan validasi khusus siswa
$user->name = $request->name; if ($user->role == 'siswa') {
$rules['nama_ortu'] = 'required';
$rules['alamat'] = 'required';
$rules['telepon'] = 'required';
}
// upload foto $request->validate($rules);
if ($request->hasFile('photo')) {
$file = $request->file('photo');
$filename = time() . '.' . $file->getClientOriginalExtension();
$file->move(public_path('uploads'), $filename);
$user->photo = $filename; // Update foto
if ($request->hasFile('photo')) {
$file = $request->file('photo');
$namaFile = time() . '.' . $file->extension();
$file->move(public_path('uploads'), $namaFile);
$user->photo = $namaFile;
}
$user->name = $request->name;
$user->save();
// Update tabel siswa jika role siswa
if ($user->role == 'siswa' && $user->siswa) {
$telepon = ltrim($request->telepon, '+');
if (strpos($telepon, '62') === 0) {
$telepon = '0' . substr($telepon, 2);
} }
$user->save(); $user->siswa->update([
'nama' => $request->name,
return back()->with('success', 'Profil berhasil diupdate'); 'nama_ortu' => $request->nama_ortu,
'alamat' => $request->alamat,
'telepon' => $telepon,
]);
} }
return back()->with('success', 'Profile berhasil diupdate');
}
} }

View File

@ -41,6 +41,18 @@ public function store(Request $request)
\Log::info('STORE DIPANGGIL'); \Log::info('STORE DIPANGGIL');
if ($request->has('telepon')) {
$telepon = $request->telepon;
// Hapus tanda plus jika ada (+62 -> 62)
$telepon = ltrim($telepon, '+');
// Jika tipenya string diawali dengan 62, ganti jadi 0
if (strpos($telepon, '62') === 0) {
$telepon = '0' . substr($telepon, 2);
}
// Masukkan kembali nilai yang sudah rapi ke dalam request
$request->merge(['telepon' => $telepon]);
}
$request->validate([ $request->validate([
'nama' => 'required', 'nama' => 'required',
'jenis_kelamin' => 'required', 'jenis_kelamin' => 'required',
@ -72,10 +84,10 @@ public function store(Request $request)
$user = User::create([ $user = User::create([
'name' => $request->nama, 'name' => $request->nama,
'email' => $request->email, 'email' => $request->email,
'password' => Hash::make('12345678'), // 🔥 default 'password' => Hash::make('12345678'), // 🔥 default
'role' => 'siswa', // atau guru 'role' => 'siswa', // atau guru
'siswa_id' => $siswa->id, 'siswa_id' => $siswa->id,
'is_default_password' => true // 🔥 WAJIB 'is_default_password' => true // 🔥 WAJIB
]); ]);
/** @var \App\Models\User $user */ /** @var \App\Models\User $user */
@ -95,6 +107,19 @@ public function edit(Siswa $siswa)
// ================= UPDATE ================= // ================= UPDATE =================
public function update(Request $request, Siswa $siswa) public function update(Request $request, Siswa $siswa)
{ {
if ($request->has('telepon')) {
$telepon = $request->telepon;
// Hapus tanda plus jika ada (+62 -> 62)
$telepon = ltrim($telepon, '+');
// Jika tipenya string diawali dengan 62, ganti jadi 0
if (strpos($telepon, '62') === 0) {
$telepon = '0' . substr($telepon, 2);
}
// Masukkan kembali nilai yang sudah rapi ke dalam request
$request->merge(['telepon' => $telepon]);
}
$request->validate([ $request->validate([
'nama' => 'required', 'nama' => 'required',
'jenis_kelamin' => 'required', 'jenis_kelamin' => 'required',

View File

@ -11,6 +11,7 @@ class OtpController extends Controller
// ================= KIRIM OTP ================= // ================= KIRIM OTP =================
public function sendOtp(Request $request) public function sendOtp(Request $request)
{ {
\Log::info('=== LOGIN OTP CALLED - OtpController ===');
$method = $request->input('method', 'email'); $method = $request->input('method', 'email');
if ($method === 'email') { if ($method === 'email') {
@ -65,73 +66,124 @@ public function sendOtp(Request $request)
// ================= LOGIN DENGAN OTP ================= // ================= LOGIN DENGAN OTP =================
public function loginOtp(Request $request) public function loginOtp(Request $request)
{ {
$request->validate([ \Log::info('=== LOGIN OTP CALLED - OtpController ===');
'email' => 'required|email',
'password' => 'required', $request->validate([
'otp' => 'nullable|digits:6', 'email' => 'required|email',
'password' => 'required',
'otp' => 'nullable|digits:6',
]);
$user = User::with(['guru', 'siswa'])->where('email', $request->email)->first();
if (!$user) {
return back()->with('error', 'Email tidak ditemukan.');
}
if (!\Hash::check($request->password, $user->password)) {
return back()->with('error', 'Password salah.');
}
// ADMIN → langsung login tanpa OTP
if ($user->role === 'admin') {
\Auth::login($user);
$request->session()->regenerate();
return redirect()->route('admin.dashboard');
}
// Cek status nonaktif
if ($user->role === 'guru' && (!$user->guru || $user->guru->status === 'nonaktif')) {
return back()->with('error', 'Akun Anda telah dinonaktifkan.');
}
if ($user->role === 'siswa' && (!$user->siswa || $user->siswa->status === 'nonaktif')) {
return back()->with('error', 'Akun Anda telah dinonaktifkan.');
}
// ── Cek cookie trusted device ──
$cookieKey = 'trusted_device_' . $user->id;
$cookieValue = $request->cookie($cookieKey);
$trustedDevices = $user->trusted_devices ?? [];
$isDeviceTrusted = $cookieValue && in_array($cookieValue, $trustedDevices);
\Log::info('=== DEVICE CHECK ===', [
'user_id' => $user->id,
'cookie_key' => $cookieKey,
'cookie_value' => $cookieValue,
'trusted_list' => $trustedDevices,
'is_trusted' => $isDeviceTrusted,
'all_cookies' => array_keys($request->cookies->all()),
]);
// Device BARU → wajib OTP
if (!$isDeviceTrusted) {
if (empty($request->otp)) {
return back()
->withInput()
->with('error', 'Perangkat baru terdeteksi! Silakan minta OTP terlebih dahulu.')
->with('need_otp', true);
}
if (!$user->otp) {
return back()->withInput()
->with('error', 'OTP belum diminta.')
->with('need_otp', true);
}
if ((string)$request->otp !== (string)$user->otp) {
return back()->withInput()
->with('error', 'Kode OTP salah.')
->with('need_otp', true);
}
if (!$user->otp_expired_at || now()->gt($user->otp_expired_at)) {
return back()->withInput()
->with('error', 'OTP sudah kadaluarsa.')
->with('need_otp', true);
}
// Generate token unik untuk device ini
$deviceToken = \Illuminate\Support\Str::random(60);
// Simpan ke trusted_devices (maks 10)
$trustedDevices[] = $deviceToken;
if (count($trustedDevices) > 10) array_shift($trustedDevices);
$user->update([
'otp' => null,
'otp_expired_at' => null,
'trusted_devices' => $trustedDevices,
]); ]);
$user = User::with(['guru', 'siswa'])->where('email', $request->email)->first();
if (!$user) {
return back()->with('error', 'User tidak ditemukan');
}
if (!\Hash::check($request->password, $user->password)) {
return back()->with('error', 'Password salah');
}
// ✅ FIX: cek status nonaktif untuk guru
if ($user->role === 'guru') {
$user->load('guru');
if (!$user->guru || $user->guru->status === 'nonaktif') {
return back()->with('error', 'Akun Anda telah dinonaktifkan. Hubungi administrator.');
}
}
// ✅ FIX BARU: cek status nonaktif untuk siswa
if ($user->role === 'siswa') {
$user->load('siswa');
if (!$user->siswa || $user->siswa->status === 'nonaktif') {
return back()->with('error', 'Akun Anda telah dinonaktifkan. Hubungi administrator.');
}
}
// Guru & Siswa wajib OTP
if (in_array($user->role, ['guru', 'siswa'])) {
if (!$request->otp) {
return back()->with('error', 'OTP wajib diisi');
}
if (!$user->otp) {
return back()->with('error', 'OTP belum diminta');
}
if ((string) $request->otp !== (string) $user->otp) {
return back()->with('error', 'OTP salah');
}
if (!$user->otp_expired_at || now()->gt($user->otp_expired_at)) {
return back()->with('error', 'OTP sudah kadaluarsa');
}
}
\Auth::login($user); \Auth::login($user);
$request->session()->regenerate(); $request->session()->regenerate();
if (in_array($user->role, ['guru', 'siswa'])) { // Simpan cookie 365 hari
$user->update(['otp' => null, 'otp_expired_at' => null]); $cookie = cookie($cookieKey, $deviceToken, 60 * 24 * 365, '/', null, false, true);
}
session()->forget('email'); \Log::info('=== DEVICE TRUSTED SAVED ===', [
'token' => $deviceToken,
'cookie_key' => $cookieKey,
]);
return match ($user->role) { return match ($user->role) {
'admin' => redirect()->route('admin.dashboard'), 'guru' => redirect()->route('guru.dashboard')->withCookie($cookie),
'guru' => redirect()->route('guru.dashboard'), 'siswa' => redirect()->route('siswa.dashboard')->withCookie($cookie),
'siswa' => redirect()->route('siswa.dashboard'), default => redirect('/')->withCookie($cookie),
default => redirect('/'),
}; };
} }
// Device LAMA → langsung login
\Log::info('=== DEVICE ALREADY TRUSTED - SKIP OTP ===');
\Auth::login($user);
$request->session()->regenerate();
return match ($user->role) {
'guru' => redirect()->route('guru.dashboard'),
'siswa' => redirect()->route('siswa.dashboard'),
default => redirect('/'),
};
}
} }

View File

@ -1,6 +1,6 @@
<?php <?php
namespace App\Http\Controllers\guru; namespace App\Http\Controllers\Guru;
use App\Models\Jadwal; use App\Models\Jadwal;
use App\Models\Absensi; use App\Models\Absensi;

View File

@ -1,6 +1,6 @@
<?php <?php
namespace App\Http\Controllers\guru; namespace App\Http\Controllers\Guru;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Jadwal; use App\Models\Jadwal;

View File

@ -200,6 +200,28 @@ public function masukkanNilaiUjian(Request $request)
return back()->with('success', 'Nilai ' . strtoupper($jenis) . ' berhasil dikirim ke menu nilai!'); return back()->with('success', 'Nilai ' . strtoupper($jenis) . ' berhasil dikirim ke menu nilai!');
} }
// ── KIRIM NILAI KE SISWA ─────────────────────────────────────────
public function kirimKeSiswa(Request $request)
{
$request->validate([
'kelas_id' => 'required',
]);
$user = auth()->user();
$mapel_id = $user->mapel_id;
$nilaiList = Nilai::with('siswa')
->where('mapel_id', $mapel_id)
->whereHas('siswa', fn($q) => $q->where('kelas_id', $request->kelas_id))
->get();
foreach ($nilaiList as $n) {
$n->update(['sudah_kirim' => true]); // ✅ ganti is_published → sudah_kirim
}
return back()->with('success', 'Nilai berhasil dikirim ke siswa!');
}
// ── EDIT ───────────────────────────────────────────────────────── // ── EDIT ─────────────────────────────────────────────────────────
public function edit($id) public function edit($id)
{ {

View File

@ -215,4 +215,52 @@ public function nilai(Request $request, $id)
return back()->with('success', 'Nilai berhasil disimpan'); return back()->with('success', 'Nilai berhasil disimpan');
} }
public function create()
{
$guru = auth()->user()->guru;
$kelas = Kelas::all();
// Ambil satu mapel pertama yang diajarkan guru ini
$mapel = Jadwal::where('guru_id', $guru->id)
->join('mata_pelajarans', 'jadwals.mata_pelajaran_id', '=', 'mata_pelajarans.id')
->select('mata_pelajarans.id', 'mata_pelajarans.nama_mapel')
->first();
return view('guru.tugas.create', compact('kelas', 'guru', 'mapel'));
}
public function store(Request $request)
{
$request->validate([
'judul' => 'required',
'kelas_id' => 'required',
'mapel_id' => 'required',
'deadline' => 'required',
'file' => 'nullable|max:10240',
], [
'mapel_id.required' => 'Mata pelajaran belum diset, hubungi admin untuk mengatur jadwal.',
'file.max' => 'Ukuran file maksimal 10MB.',
]);
$guru = auth()->user()->guru;
$filePath = null;
if ($request->hasFile('file')) {
$filePath = $request->file('file')->store('tugas_file', 'public');
}
Tugas::create([
'judul' => $request->judul,
'deskripsi' => $request->deskripsi,
'kelas_id' => $request->kelas_id,
'mapel_id' => $request->mapel_id, // ✅ sesuaikan nama kolom
'guru_id' => $guru->id,
'deadline' => $request->deadline,
'file' => $filePath,
]);
return redirect()->route('guru.tugas.index')
->with('success', 'Tugas berhasil dibuat');
}
} }

View File

@ -0,0 +1,15 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
class EncryptCookies extends Middleware
{
/**
* Cookie yang tidak dienkripsi
*/
protected $except = [
//
];
}

View File

@ -29,8 +29,9 @@ class User extends Authenticatable
'photo', 'photo',
'otp', 'otp',
'otp_expired_at', 'otp_expired_at',
'telepon', // 🔥 WAJIB TAMBAH INI 'telepon',
'is_default_password' 'is_default_password',
'trusted_devices', // ← tambahkan ini
]; ];
/** /**
@ -47,6 +48,7 @@ class User extends Authenticatable
protected $casts = [ protected $casts = [
'email_verified_at' => 'datetime', 'email_verified_at' => 'datetime',
'password' => 'hashed', 'password' => 'hashed',
'trusted_devices' => 'array',
]; ];
/* /*
@ -94,7 +96,7 @@ public function mapel()
return $this->belongsTo(MataPelajaran::class, 'mapel_id'); return $this->belongsTo(MataPelajaran::class, 'mapel_id');
} }
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@ -104,20 +106,20 @@ public function mapel()
// Ambil nomor WA dengan fallback // Ambil nomor WA dengan fallback
public function getTeleponLengkap() public function getTeleponLengkap()
{ {
if ($this->telepon) { if ($this->telepon) {
return $this->telepon; return $this->telepon;
} }
if ($this->guru && $this->guru->telepon) { if ($this->guru && $this->guru->telepon) {
return $this->guru->telepon; return $this->guru->telepon;
} }
// 🔥 fallback tambahan (opsional tapi aman) // 🔥 fallback tambahan (opsional tapi aman)
if ($this->siswa && $this->siswa->telepon) { if ($this->siswa && $this->siswa->telepon) {
return $this->siswa->telepon; return $this->siswa->telepon;
} }
return null; return null;
} }
} }

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->json('trusted_devices')->nullable()->after('otp_expired_at');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
//
});
}
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

View File

@ -150,17 +150,47 @@
</div> </div>
</div> </div>
{{-- JAM --}} {{-- JAM --}}
<div class="field-row"> <div class="field-row">
<div class="field"> <div class="field">
<label class="field-label">Jam Mulai</label> <label class="field-label">Jam Mulai</label>
<input type="time" name="jam_mulai" value="{{ old('jam_mulai') }}" class="field-input"> <div class="select-wrap">
</div> <select name="jam_mulai" class="field-select">
<div class="field"> <option value="">-- Pilih Jam Mulai --</option>
<label class="field-label">Jam Selesai</label> @php
<input type="time" name="jam_selesai" value="{{ old('jam_selesai') }}" class="field-input"> $slots = [];
</div> $start = strtotime('07:00');
</div> $end = strtotime('15:10');
for ($t = $start; $t <= $end; $t += 10 * 60) {
$slots[] = date('H:i', $t);
if (($t + 5 * 60) <= $end) {
$slots[] = date('H:i', $t + 5 * 60);
}
}
sort($slots);
@endphp
@foreach($slots as $slot)
<option value="{{ $slot }}" {{ old('jam_mulai') == $slot ? 'selected' : '' }}>
{{ $slot }}
</option>
@endforeach
</select>
</div>
</div>
<div class="field">
<label class="field-label">Jam Selesai</label>
<div class="select-wrap">
<select name="jam_selesai" class="field-select">
<option value="">-- Pilih Jam Selesai --</option>
@foreach($slots as $slot)
<option value="{{ $slot }}" {{ old('jam_selesai') == $slot ? 'selected' : '' }}>
{{ $slot }}
</option>
@endforeach
</select>
</div>
</div>
</div>
<div class="form-footer"> <div class="form-footer">
<button type="submit" class="btn-submit"><i class="fas fa-save"></i> Simpan</button> <button type="submit" class="btn-submit"><i class="fas fa-save"></i> Simpan</button>

View File

@ -171,17 +171,47 @@
</div> </div>
</div> </div>
{{-- JAM --}} {{-- JAM --}}
<div class="field-row"> <div class="field-row">
<div class="field"> <div class="field">
<label class="field-label">Jam Mulai</label> <label class="field-label">Jam Mulai</label>
<input type="time" name="jam_mulai" value="{{ $jadwal->jam_mulai }}" class="field-input" required> <div class="select-wrap">
</div> <select name="jam_mulai" class="field-select" required>
<div class="field"> <option value="">-- Pilih Jam Mulai --</option>
<label class="field-label">Jam Selesai</label> @php
<input type="time" name="jam_selesai" value="{{ $jadwal->jam_selesai }}" class="field-input" required> $slots = [];
</div> $start = strtotime('07:00');
</div> $end = strtotime('15:10');
for ($t = $start; $t <= $end; $t += 10 * 60) {
$slots[] = date('H:i', $t);
if (($t + 5 * 60) <= $end) {
$slots[] = date('H:i', $t + 5 * 60);
}
}
sort($slots);
@endphp
@foreach($slots as $slot)
<option value="{{ $slot }}" {{ old('jam_mulai', \Illuminate\Support\Str::substr($jadwal->jam_mulai, 0, 5)) == $slot ? 'selected' : '' }}>
{{ $slot }}
</option>
@endforeach
</select>
</div>
</div>
<div class="field">
<label class="field-label">Jam Selesai</label>
<div class="select-wrap">
<select name="jam_selesai" class="field-select" required>
<option value="">-- Pilih Jam Selesai --</option>
@foreach($slots as $slot)
<option value="{{ $slot }}" {{ old('jam_selesai', \Illuminate\Support\Str::substr($jadwal->jam_selesai, 0, 5)) == $slot ? 'selected' : '' }}>
{{ $slot }}
</option>
@endforeach
</select>
</div>
</div>
</div>
<div class="form-footer"> <div class="form-footer">
<button type="submit" class="btn-submit"><i class="fas fa-save"></i> Update</button> <button type="submit" class="btn-submit"><i class="fas fa-save"></i> Update</button>

View File

@ -11,8 +11,6 @@
--primary-light: #EEF2FF; --primary-light: #EEF2FF;
--success: #059669; --success: #059669;
--success-light: #ECFDF5; --success-light: #ECFDF5;
--danger: #DC2626;
--neutral-light: #F9FAFB;
--bg: #F3F4F8; --bg: #F3F4F8;
--surface: #FFFFFF; --surface: #FFFFFF;
--border: #E5E7EB; --border: #E5E7EB;
@ -25,10 +23,11 @@
* { font-family: 'Plus Jakarta Sans', sans-serif; box-sizing: border-box; } * { font-family: 'Plus Jakarta Sans', sans-serif; box-sizing: border-box; }
/* ── PAGE ── */
.sw-page { padding: 28px 32px; background: var(--bg); min-height: 100vh; } .sw-page { padding: 28px 32px; background: var(--bg); min-height: 100vh; }
/* ── TOPBAR ── */ /* ── TOPBAR ── */
.sw-topbar { margin-bottom: 28px; } .sw-topbar { margin-bottom: 24px; }
.sw-topbar h3 { font-size: 21px; font-weight: 800; color: var(--text-dark); margin: 0 0 4px; letter-spacing: -.3px; } .sw-topbar h3 { font-size: 21px; font-weight: 800; color: var(--text-dark); margin: 0 0 4px; letter-spacing: -.3px; }
.sw-topbar p { font-size: 13px; color: var(--text-soft); margin: 0; } .sw-topbar p { font-size: 13px; color: var(--text-soft); margin: 0; }
@ -40,7 +39,7 @@
font-size: 13px; color: var(--success); margin-bottom: 24px; font-weight: 500; font-size: 13px; color: var(--success); margin-bottom: 24px; font-weight: 500;
} }
/* ── GRID LAYOUT ── */ /* ── GRID ── */
.sw-grid { .sw-grid {
display: grid; display: grid;
grid-template-columns: 300px 1fr; grid-template-columns: 300px 1fr;
@ -53,24 +52,15 @@
background: var(--surface); background: var(--surface);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
border: 1px solid var(--border); border: 1px solid var(--border);
box-shadow: 0 1px 3px rgba(0,0,0,.05);
overflow: hidden; overflow: hidden;
} }
/* ── AVATAR CARD ── */ /* ── AVATAR CARD ── */
.avatar-card { text-align: center; }
.avatar-banner { .avatar-banner {
height: 90px; height: 90px;
background: linear-gradient(135deg, #1E3A6E 0%, #4F46E5 60%, #7C3AED 100%); background: linear-gradient(135deg, #1E3A6E 0%, #4F46E5 60%, #7C3AED 100%);
position: relative;
} }
.avatar-banner::after { .avatar-body { padding: 0 24px 28px; text-align: center; }
content: '';
position: absolute; inset: 0;
background: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.04'%3E%3Ccircle cx='30' cy='30' r='20'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
}
.avatar-wrap { .avatar-wrap {
position: relative; position: relative;
display: inline-block; display: inline-block;
@ -79,81 +69,70 @@
} }
.avatar-img { .avatar-img {
width: 100px; height: 100px; width: 100px; height: 100px;
border-radius: 50%; border-radius: 50%; object-fit: cover;
object-fit: cover; border: 4px solid var(--surface); display: block;
border: 4px solid var(--surface);
box-shadow: 0 4px 16px rgba(0,0,0,.12);
display: block;
} }
.avatar-online { .avatar-online {
position: absolute; bottom: 6px; right: 6px; position: absolute; bottom: 6px; right: 6px;
width: 14px; height: 14px; width: 14px; height: 14px;
background: var(--success); background: var(--success); border: 2px solid var(--surface); border-radius: 50%;
border: 2px solid var(--surface);
border-radius: 50%;
} }
.avatar-name { font-size: 17px; font-weight: 800; color: var(--text-dark); margin-bottom: 6px; }
.avatar-body { padding: 0 24px 28px; }
.avatar-name { font-size: 17px; font-weight: 800; color: var(--text-dark); margin-bottom: 4px; }
.avatar-role { .avatar-role {
display: inline-flex; align-items: center; gap: 5px; display: inline-flex; align-items: center; gap: 5px;
background: var(--primary-light); border: 1px solid #C7D2FE; background: var(--primary-light); border: 1px solid #C7D2FE;
border-radius: 99px; padding: 4px 12px; border-radius: 99px; padding: 4px 12px;
font-size: 11.5px; font-weight: 700; color: var(--primary); font-size: 11.5px; font-weight: 700; color: var(--primary); margin-bottom: 20px;
margin-bottom: 20px;
} }
.avatar-role span { width: 6px; height: 6px; background: var(--primary); border-radius: 50%; } .avatar-role span { width: 6px; height: 6px; background: var(--primary); border-radius: 50%; }
.avatar-stats { .avatar-stats {
display: flex; gap: 0; display: flex;
border: 1px solid var(--border); border: 1px solid var(--border); border-radius: var(--radius-md); overflow: hidden;
border-radius: var(--radius-md);
overflow: hidden;
}
.avatar-stat {
flex: 1; padding: 12px 8px; text-align: center;
border-right: 1px solid var(--border);
} }
.avatar-stat { flex: 1; padding: 12px 8px; text-align: center; border-right: 1px solid var(--border); }
.avatar-stat:last-child { border-right: none; } .avatar-stat:last-child { border-right: none; }
.avatar-stat-val { font-size: 16px; font-weight: 800; color: var(--primary); } .avatar-stat-val { display: flex; justify-content: center; margin-bottom: 4px; }
.avatar-stat-lbl { font-size: 10.5px; color: var(--text-soft); font-weight: 600; margin-top: 2px; } .avatar-stat-lbl { font-size: 10.5px; color: var(--text-soft); font-weight: 600; }
/* ── FORM CARD ── */ /* ── INFO LIST (siswa) ── */
.avatar-info-list { margin-top: 18px; display: flex; flex-direction: column; gap: 10px; text-align: left; }
.avatar-info-item {
display: flex; align-items: flex-start; gap: 10px;
padding: 10px 12px;
background: #F9FAFB; border: 1px solid var(--border); border-radius: var(--radius-md);
}
.avatar-info-item svg { color: var(--primary); flex-shrink: 0; margin-top: 1px; }
.avatar-info-item .lbl { font-size: 10.5px; font-weight: 700; color: var(--text-soft); text-transform: uppercase; letter-spacing: .5px; margin-bottom: 2px; }
.avatar-info-item .val { font-size: 13px; font-weight: 600; color: var(--text-dark); }
/* ── CARD HEADER ── */
.sw-card-header { .sw-card-header {
display: flex; align-items: center; gap: 12px; display: flex; align-items: center; gap: 12px;
padding: 20px 24px; padding: 20px 24px; border-bottom: 1px solid var(--border);
border-bottom: 1px solid var(--border); background: #F5F3FF;
background: linear-gradient(135deg, #F5F3FF 0%, #EEF2FF 100%);
} }
.sw-card-header-icon { .sw-card-header-icon {
width: 38px; height: 38px; width: 38px; height: 38px;
background: var(--primary); border-radius: 10px; background: var(--primary); border-radius: 10px;
display: flex; align-items: center; justify-content: center; display: flex; align-items: center; justify-content: center;
color: #fff; flex-shrink: 0; color: #fff; flex-shrink: 0;
box-shadow: 0 2px 8px rgba(79,70,229,.3);
} }
.sw-card-header h5 { font-size: 15px; font-weight: 700; color: #3730A3; margin: 0; } .sw-card-header h5 { font-size: 15px; font-weight: 700; color: #3730A3; margin: 0; }
.sw-card-header p { font-size: 12.5px; color: #6D6AA4; margin: 2px 0 0; } .sw-card-header p { font-size: 12.5px; color: #6D6AA4; margin: 2px 0 0; }
.sw-card-body { padding: 28px 24px; } .sw-card-body { padding: 28px 24px; }
/* ── SECTION ── */ /* ── SECTION DIVIDER ── */
.sw-section { .sw-section { display: flex; align-items: center; gap: 10px; margin-bottom: 20px; }
display: flex; align-items: center; gap: 10px; .sw-section-label { font-size: 11.5px; font-weight: 700; color: var(--primary); text-transform: uppercase; letter-spacing: .8px; white-space: nowrap; }
margin: 0 0 20px; .sw-section-line { flex: 1; height: 1px; background: linear-gradient(to right, #C7D2FE, transparent); }
}
.sw-section-label {
font-size: 11.5px; font-weight: 700; color: var(--primary);
text-transform: uppercase; letter-spacing: .8px; white-space: nowrap;
}
.sw-section-line {
flex: 1; height: 1px;
background: linear-gradient(to right, #C7D2FE, transparent);
}
/* ── FORM ELEMENTS ── */ /* ── FORM ── */
.sw-form-group { display: flex; flex-direction: column; gap: 6px; margin-bottom: 20px; } .sw-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.sw-label { font-size: 12.5px; font-weight: 600; color: var(--text-mid); } .sw-form-group { display: flex; flex-direction: column; gap: 6px; margin-bottom: 18px; }
.sw-label { font-size: 12.5px; font-weight: 600; color: var(--text-mid); }
.sw-input-note { font-size: 11px; color: var(--text-soft); }
.sw-input-wrap { position: relative; } .sw-input-wrap { position: relative; }
.sw-input-wrap svg { .sw-input-wrap svg {
@ -161,38 +140,41 @@
transform: translateY(-50%); color: #9CA3AF; pointer-events: none; transform: translateY(-50%); color: #9CA3AF; pointer-events: none;
} }
.sw-input { .sw-input {
width: 100%; width: 100%; padding: 11px 14px 11px 42px;
padding: 11px 14px 11px 42px; border: 1.5px solid var(--border); border-radius: var(--radius-md);
border: 1.5px solid var(--border);
border-radius: var(--radius-md);
font-size: 14px; font-family: 'Plus Jakarta Sans', sans-serif; font-size: 14px; font-family: 'Plus Jakarta Sans', sans-serif;
color: var(--text-dark); background: var(--neutral-light); color: var(--text-dark); background: #F9FAFB;
outline: none; transition: all .15s; outline: none; transition: all .15s;
} }
.sw-input:focus { .sw-input:focus { border-color: var(--primary); background: #fff; box-shadow: 0 0 0 3px rgba(79,70,229,.1); }
border-color: var(--primary); .sw-input[readonly] { background: #F3F4F8; color: var(--text-soft); cursor: not-allowed; }
background: #fff;
box-shadow: 0 0 0 3px rgba(79,70,229,.1); .sw-textarea {
} width: 100%; padding: 11px 14px;
border: 1.5px solid var(--border); border-radius: var(--radius-md);
font-size: 14px; font-family: 'Plus Jakarta Sans', sans-serif;
color: var(--text-dark); background: #F9FAFB;
outline: none; resize: vertical; min-height: 80px; transition: all .15s;
}
.sw-textarea:focus { border-color: var(--primary); background: #fff; box-shadow: 0 0 0 3px rgba(79,70,229,.1); }
/* ── PHOTO ── */
.sw-photo-preview {
display: flex; align-items: center; gap: 14px;
background: #F9FAFB; border: 1px solid var(--border);
border-radius: var(--radius-md); padding: 14px 16px; margin-bottom: 12px;
}
.sw-photo-preview img { width: 52px; height: 52px; border-radius: 50%; object-fit: cover; border: 2px solid #C7D2FE; }
.sw-photo-preview-info .title { font-size: 13px; font-weight: 700; color: var(--text-dark); }
.sw-photo-preview-info .sub { font-size: 11.5px; color: var(--text-soft); margin-top: 2px; }
/* ── PHOTO UPLOAD ZONE ── */
.sw-upload-zone { .sw-upload-zone {
border: 2px dashed var(--border); border: 2px dashed var(--border); border-radius: var(--radius-md);
border-radius: var(--radius-md); padding: 24px 20px; text-align: center;
padding: 24px 20px; background: #F9FAFB; cursor: pointer; transition: all .2s; position: relative;
text-align: center;
background: var(--neutral-light);
cursor: pointer;
transition: all .2s;
position: relative;
}
.sw-upload-zone:hover {
border-color: var(--primary);
background: var(--primary-light);
}
.sw-upload-zone input[type="file"] {
position: absolute; inset: 0; opacity: 0; cursor: pointer; width: 100%; height: 100%;
} }
.sw-upload-zone:hover { border-color: var(--primary); background: var(--primary-light); }
.sw-upload-zone input[type="file"] { position: absolute; inset: 0; opacity: 0; cursor: pointer; width: 100%; height: 100%; }
.sw-upload-icon { .sw-upload-icon {
width: 44px; height: 44px; width: 44px; height: 44px;
background: var(--primary-light); border-radius: 10px; background: var(--primary-light); border-radius: 10px;
@ -208,40 +190,24 @@
font-size: 11px; font-weight: 600; color: var(--primary); font-size: 11px; font-weight: 600; color: var(--primary);
} }
/* current photo preview */
.sw-photo-preview {
display: flex; align-items: center; gap: 14px;
background: var(--neutral-light); border: 1px solid var(--border);
border-radius: var(--radius-md); padding: 14px 16px;
margin-bottom: 12px;
}
.sw-photo-preview img {
width: 52px; height: 52px;
border-radius: 50%; object-fit: cover;
border: 2px solid var(--primary-light);
}
.sw-photo-preview-info .title { font-size: 13px; font-weight: 700; color: var(--text-dark); }
.sw-photo-preview-info .sub { font-size: 11.5px; color: var(--text-soft); margin-top: 2px; }
/* ── FOOTER ── */ /* ── FOOTER ── */
.sw-footer { .sw-footer {
display: flex; justify-content: flex-end; align-items: center; display: flex; justify-content: flex-end;
padding-top: 24px; border-top: 1px solid var(--border); margin-top: 8px; padding-top: 24px; border-top: 1px solid var(--border); margin-top: 8px;
} }
.sw-btn { .sw-btn-primary {
display: inline-flex; align-items: center; gap: 7px; display: inline-flex; align-items: center; gap: 7px;
padding: 11px 22px; border-radius: var(--radius-md); padding: 11px 22px; border-radius: var(--radius-md);
font-size: 14px; font-weight: 700; border: none; font-size: 14px; font-weight: 700; border: none; cursor: pointer;
cursor: pointer; text-decoration: none; background: var(--primary); color: #fff;
transition: all .18s ease; line-height: 1; font-family: 'Plus Jakarta Sans', sans-serif; transition: all .18s;
font-family: 'Plus Jakarta Sans', sans-serif;
} }
.sw-btn-primary { background: var(--primary); color: #fff; box-shadow: 0 4px 14px rgba(79,70,229,.3); } .sw-btn-primary:hover { background: var(--primary-dark); transform: translateY(-1px); }
.sw-btn-primary:hover { background: var(--primary-dark); transform: translateY(-1px); box-shadow: 0 6px 20px rgba(79,70,229,.38); }
@media (max-width: 900px) { @media (max-width: 900px) {
.sw-grid { grid-template-columns: 1fr; } .sw-grid { grid-template-columns: 1fr; }
.sw-page { padding: 16px; } .sw-page { padding: 16px; }
.sw-row { grid-template-columns: 1fr; }
} }
</style> </style>
@ -263,10 +229,11 @@
<div class="sw-grid"> <div class="sw-grid">
{{-- LEFT: AVATAR CARD --}} {{-- KIRI: AVATAR CARD --}}
<div class="sw-card avatar-card"> <div class="sw-card">
<div class="avatar-banner"></div> <div class="avatar-banner"></div>
<div class="avatar-body"> <div class="avatar-body">
<div class="avatar-wrap"> <div class="avatar-wrap">
@if(auth()->user()->photo) @if(auth()->user()->photo)
<img src="{{ asset('uploads/' . auth()->user()->photo) }}" class="avatar-img" alt="Foto Profil"> <img src="{{ asset('uploads/' . auth()->user()->photo) }}" class="avatar-img" alt="Foto Profil">
@ -278,7 +245,11 @@
<div class="avatar-name">{{ auth()->user()->name }}</div> <div class="avatar-name">{{ auth()->user()->name }}</div>
<div class="avatar-role"> <div class="avatar-role">
<span></span> Administrator <span></span>
@if(auth()->user()->role == 'siswa') Siswa
@elseif(auth()->user()->role == 'guru') Guru
@else Administrator
@endif
</div> </div>
<div class="avatar-stats"> <div class="avatar-stats">
@ -301,11 +272,40 @@
<div class="avatar-stat-lbl">Aktif</div> <div class="avatar-stat-lbl">Aktif</div>
</div> </div>
</div> </div>
{{-- Info ringkas khusus siswa --}}
@if(auth()->user()->role == 'siswa' && auth()->user()->siswa)
@php $siswa = auth()->user()->siswa; @endphp
<div class="avatar-info-list">
<div class="avatar-info-item">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="7" width="20" height="14" rx="2"/><path d="M16 3H8L2 7h20l-6-4z"/></svg>
<div><div class="lbl">NIS</div><div class="val">{{ $siswa->nis }}</div></div>
</div>
<div class="avatar-info-item">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
<div><div class="lbl">Kelas</div><div class="val">{{ $siswa->kelas->nama_kelas ?? '-' }}</div></div>
</div>
<div class="avatar-info-item">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
<div><div class="lbl">Orang Tua</div><div class="val">{{ $siswa->nama_ortu }}</div></div>
</div>
<div class="avatar-info-item">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07A19.5 19.5 0 0 1 4.69 13a19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 3.6 2.18h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L7.91 9.91a16 16 0 0 0 6.06 6.06l.94-.93a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 22 16.92z"/></svg>
<div><div class="lbl">Telepon</div><div class="val">{{ $siswa->telepon }}</div></div>
</div>
<div class="avatar-info-item">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>
<div><div class="lbl">Alamat</div><div class="val">{{ $siswa->alamat }}</div></div>
</div>
</div>
@endif
</div> </div>
</div> </div>
{{-- RIGHT: FORM CARD --}} {{-- KANAN: FORM CARD --}}
<div class="sw-card"> <div class="sw-card">
<div class="sw-card-header"> <div class="sw-card-header">
<div class="sw-card-header-icon"> <div class="sw-card-header-icon">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
@ -320,7 +320,7 @@
<form action="{{ route('admin.profile.update') }}" method="POST" enctype="multipart/form-data"> <form action="{{ route('admin.profile.update') }}" method="POST" enctype="multipart/form-data">
@csrf @csrf
{{-- SECTION: INFO --}} {{-- Informasi Akun --}}
<div class="sw-section"> <div class="sw-section">
<span class="sw-section-label">Informasi Akun</span> <span class="sw-section-label">Informasi Akun</span>
<div class="sw-section-line"></div> <div class="sw-section-line"></div>
@ -330,18 +330,68 @@
<label class="sw-label">Nama Lengkap</label> <label class="sw-label">Nama Lengkap</label>
<div class="sw-input-wrap"> <div class="sw-input-wrap">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
<input type="text" name="name" class="sw-input" <input type="text" name="name" class="sw-input" value="{{ auth()->user()->name }}" required>
value="{{ auth()->user()->name }}" required>
</div> </div>
</div> </div>
{{-- SECTION: FOTO --}} {{-- Data Diri (khusus siswa) --}}
<div class="sw-section" style="margin-top:28px;"> @if(auth()->user()->role == 'siswa' && auth()->user()->siswa)
@php $siswa = auth()->user()->siswa; @endphp
<div class="sw-section" style="margin-top: 8px;">
<span class="sw-section-label">Data Diri</span>
<div class="sw-section-line"></div>
</div>
<div class="sw-row">
<div class="sw-form-group">
<label class="sw-label">NIS</label>
<div class="sw-input-wrap">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="7" width="20" height="14" rx="2"/><path d="M16 3H8L2 7h20l-6-4z"/></svg>
<input type="text" class="sw-input" value="{{ $siswa->nis }}" readonly>
</div>
<span class="sw-input-note">NIS tidak dapat diubah</span>
</div>
<div class="sw-form-group">
<label class="sw-label">Kelas</label>
<div class="sw-input-wrap">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/></svg>
<input type="text" class="sw-input" value="{{ $siswa->kelas->nama_kelas ?? '-' }}" readonly>
</div>
<span class="sw-input-note">Ditentukan oleh admin</span>
</div>
</div>
<div class="sw-row">
<div class="sw-form-group">
<label class="sw-label">Nama Orang Tua</label>
<div class="sw-input-wrap">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
<input type="text" name="nama_ortu" class="sw-input" value="{{ $siswa->nama_ortu }}" required>
</div>
</div>
<div class="sw-form-group">
<label class="sw-label">Nomor Telepon</label>
<div class="sw-input-wrap">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07A19.5 19.5 0 0 1 4.69 13a19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 3.6 2.18h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L7.91 9.91a16 16 0 0 0 6.06 6.06l.94-.93a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 22 16.92z"/></svg>
<input type="text" name="telepon" class="sw-input" value="{{ $siswa->telepon }}" required>
</div>
</div>
</div>
<div class="sw-form-group">
<label class="sw-label">Alamat Lengkap</label>
<textarea name="alamat" class="sw-textarea" required>{{ $siswa->alamat }}</textarea>
</div>
@endif
{{-- Foto Profil --}}
<div class="sw-section" style="margin-top: 28px;">
<span class="sw-section-label">Foto Profil</span> <span class="sw-section-label">Foto Profil</span>
<div class="sw-section-line"></div> <div class="sw-section-line"></div>
</div> </div>
{{-- Current photo preview --}}
<div class="sw-photo-preview"> <div class="sw-photo-preview">
@if(auth()->user()->photo) @if(auth()->user()->photo)
<img src="{{ asset('uploads/' . auth()->user()->photo) }}" alt="Foto saat ini"> <img src="{{ asset('uploads/' . auth()->user()->photo) }}" alt="Foto saat ini">
@ -354,20 +404,18 @@
</div> </div>
</div> </div>
{{-- Upload zone --}}
<div class="sw-upload-zone"> <div class="sw-upload-zone">
<input type="file" name="photo" accept="image/*"> <input type="file" name="photo" accept="image/*">
<div class="sw-upload-icon"> <div class="sw-upload-icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
</div> </div>
<div class="sw-upload-title">Klik untuk unggah foto baru</div> <div class="sw-upload-title">Klik untuk unggah foto baru</div>
<div class="sw-upload-sub">atau seret & lepas file ke sini</div> <div class="sw-upload-sub">atau seret &amp; lepas file ke sini</div>
<div class="sw-upload-badge">Pilih File</div> <div class="sw-upload-badge">Pilih File</div>
</div> </div>
{{-- FOOTER --}}
<div class="sw-footer"> <div class="sw-footer">
<button type="submit" class="sw-btn sw-btn-primary"> <button type="submit" class="sw-btn-primary">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.3"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg> <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.3"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>
Update Profil Update Profil
</button> </button>
@ -381,7 +429,6 @@
</div> </div>
<script> <script>
// Live preview when file is selected
const fileInput = document.querySelector('input[type="file"]'); const fileInput = document.querySelector('input[type="file"]');
const previewImg = document.querySelector('.sw-photo-preview img'); const previewImg = document.querySelector('.sw-photo-preview img');
const uploadTitle = document.querySelector('.sw-upload-title'); const uploadTitle = document.querySelector('.sw-upload-title');

View File

@ -2,407 +2,600 @@
@section('content') @section('content')
<style> <style>
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap');
:root { :root {
--primary: #4F46E5; --primary: #4F46E5;
--primary-light: #EEF2FF; --primary-light: #EEF2FF;
--primary-dark: #3730A3; --primary-dark: #3730A3;
--success: #059669; --success: #059669;
--success-light: #ECFDF5; --success-light: #ECFDF5;
--danger: #DC2626; --danger: #DC2626;
--danger-light: #FEF2F2; --danger-light: #FEF2F2;
--warning: #D97706; --warning: #D97706;
--warning-light: #FFFBEB; --warning-light: #FFFBEB;
--info: #0284C7; --info: #0284C7;
--info-light: #F0F9FF; --info-light: #F0F9FF;
--neutral: #6B7280; --neutral: #6B7280;
--neutral-light: #F9FAFB; --neutral-light: #F9FAFB;
--bg: #F3F4F8; --bg: #F3F4F8;
--surface: #FFFFFF; --surface: #FFFFFF;
--border: #E5E7EB; --border: #E5E7EB;
--text-dark: #111827; --text-dark: #111827;
--text-mid: #374151; --text-mid: #374151;
--text-soft: #6B7280; --text-soft: #6B7280;
--radius-md: 10px; --radius-md: 10px;
--radius-lg: 14px; --radius-lg: 14px;
--shadow-sm: 0 1px 3px rgba(0,0,0,.06), 0 1px 2px rgba(0,0,0,.04); --shadow-sm: 0 1px 3px rgba(0, 0, 0, .06), 0 1px 2px rgba(0, 0, 0, .04);
--shadow-md: 0 4px 16px rgba(0,0,0,.07); --shadow-md: 0 4px 16px rgba(0, 0, 0, .07);
} }
* { font-family: 'Plus Jakarta Sans', sans-serif; box-sizing: border-box; } * {
font-family: 'Plus Jakarta Sans', sans-serif;
box-sizing: border-box;
}
/* ── PAGE WRAPPER ── */ /* ── PAGE WRAPPER ── */
.sw-page { padding: 28px 32px; background: var(--bg); min-height: 100vh; } .sw-page {
padding: 28px 32px;
background: var(--bg);
min-height: 100vh;
}
/* ── TOPBAR ── */ /* ── TOPBAR ── */
.sw-topbar { .sw-topbar {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
margin-bottom: 24px; margin-bottom: 24px;
} }
.sw-topbar-left h3 {
font-size: 22px;
font-weight: 700;
color: var(--text-dark);
margin: 0;
letter-spacing: -.3px;
}
.sw-topbar-left p {
font-size: 13px;
color: var(--text-soft);
margin: 2px 0 0;
}
/* ── BUTTON ── */ .sw-topbar-left h3 {
.sw-btn { font-size: 22px;
display: inline-flex; font-weight: 700;
align-items: center; color: var(--text-dark);
gap: 7px; margin: 0;
padding: 9px 18px; letter-spacing: -.3px;
border-radius: var(--radius-md); }
font-size: 13.5px;
font-weight: 600;
border: none;
cursor: pointer;
text-decoration: none;
transition: all .18s ease;
line-height: 1;
}
.sw-btn-primary { background: var(--primary); color: #fff; }
.sw-btn-primary:hover { background: var(--primary-dark); color: #fff; transform: translateY(-1px); box-shadow: 0 4px 12px rgba(79,70,229,.35); }
.sw-btn-secondary { background: var(--surface); color: var(--text-mid); border: 1px solid var(--border); }
.sw-btn-secondary:hover { background: var(--neutral-light); color: var(--text-mid); }
.sw-btn-danger { background: var(--danger-light); color: var(--danger); border: 1px solid #FECACA; }
.sw-btn-danger:hover { background: var(--danger); color: #fff; }
.sw-btn-warning { background: var(--warning-light); color: var(--warning); border: 1px solid #FDE68A; }
.sw-btn-warning:hover { background: var(--warning); color: #fff; }
.sw-btn-success { background: var(--success-light); color: var(--success); border: 1px solid #A7F3D0; }
.sw-btn-success:hover { background: var(--success); color: #fff; }
.sw-btn-sm { padding: 6px 12px; font-size: 12.5px; border-radius: 8px; }
/* ── ALERT ── */ .sw-topbar-left p {
.sw-alert { font-size: 13px;
display: flex; color: var(--text-soft);
align-items: center; margin: 2px 0 0;
gap: 10px; }
padding: 13px 16px;
border-radius: var(--radius-md);
font-size: 13.5px;
font-weight: 500;
margin-bottom: 20px;
background: var(--success-light);
color: var(--success);
border: 1px solid #A7F3D0;
}
.sw-alert svg { flex-shrink: 0; }
/* ── FILTER CARD ── */ /* ── BUTTON ── */
.sw-card { .sw-btn {
background: var(--surface); display: inline-flex;
border-radius: var(--radius-lg); align-items: center;
box-shadow: var(--shadow-sm); gap: 7px;
border: 1px solid var(--border); padding: 9px 18px;
} border-radius: var(--radius-md);
.sw-filter { font-size: 13.5px;
padding: 16px 20px; font-weight: 600;
margin-bottom: 18px; border: none;
} cursor: pointer;
.sw-filter form { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; } text-decoration: none;
.sw-filter label { font-size: 13px; font-weight: 600; color: var(--text-mid); white-space: nowrap; } transition: all .18s ease;
.sw-filter .form-select { line-height: 1;
border: 1px solid var(--border); }
border-radius: 8px;
padding: 8px 14px;
font-size: 13px;
color: var(--text-mid);
background: var(--neutral-light);
font-family: 'Plus Jakarta Sans', sans-serif;
outline: none;
transition: border-color .15s;
}
.sw-filter .form-select:focus { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(79,70,229,.1); }
/* ── BULK BAR ── */ .sw-btn-primary {
.sw-bulk-bar { background: var(--primary);
display: flex; color: #fff;
align-items: center; }
gap: 10px;
margin-bottom: 14px;
}
/* ── TABLE CARD ── */ .sw-btn-primary:hover {
.sw-table-card { overflow: hidden; } background: var(--primary-dark);
.sw-table-wrap { overflow-x: auto; } color: #fff;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(79, 70, 229, .35);
}
table.sw-table { .sw-btn-secondary {
width: 100%; background: var(--surface);
border-collapse: collapse; color: var(--text-mid);
font-size: 13.5px; border: 1px solid var(--border);
} }
table.sw-table thead tr { .sw-btn-secondary:hover {
background: linear-gradient(135deg, #F5F3FF 0%, #EEF2FF 100%); background: var(--neutral-light);
border-bottom: 2px solid #DDD6FE; color: var(--text-mid);
} }
table.sw-table thead th {
padding: 13px 16px;
font-weight: 700;
font-size: 12px;
color: #5B21B6;
text-transform: uppercase;
letter-spacing: .6px;
white-space: nowrap;
border: none;
}
table.sw-table tbody tr {
border-bottom: 1px solid var(--border);
transition: background .15s;
}
table.sw-table tbody tr:last-child { border-bottom: none; }
table.sw-table tbody tr:hover { background: #FAFAFF; }
table.sw-table td {
padding: 13px 16px;
color: var(--text-mid);
border: none;
vertical-align: middle;
}
table.sw-table td.center { text-align: center; }
table.sw-table td.name-cell { font-weight: 600; color: var(--text-dark); }
/* ── CUSTOM CHECKBOX ── */ .sw-btn-danger {
.sw-check { background: var(--danger-light);
width: 16px; height: 16px; color: var(--danger);
accent-color: var(--primary); border: 1px solid #FECACA;
cursor: pointer; }
}
/* ── BADGES ── */ .sw-btn-danger:hover {
.sw-badge { background: var(--danger);
display: inline-flex; color: #fff;
align-items: center; }
gap: 4px;
padding: 4px 10px;
border-radius: 99px;
font-size: 11.5px;
font-weight: 600;
letter-spacing: .1px;
}
.sw-badge-success { background: var(--success-light); color: var(--success); }
.sw-badge-danger { background: var(--danger-light); color: var(--danger); }
.sw-badge-primary { background: var(--primary-light); color: var(--primary); }
.sw-badge-neutral { background: #F3F4F6; color: #6B7280; }
.sw-badge-info { background: var(--info-light); color: var(--info); }
/* ── NONAKTIF INLINE FORM ── */ .sw-btn-warning {
.sw-nonaktif-form { background: var(--warning-light);
display: flex; color: var(--warning);
gap: 6px; border: 1px solid #FDE68A;
align-items: center; }
}
.sw-nonaktif-input {
width: 110px;
padding: 6px 10px;
border: 1px solid var(--border);
border-radius: 7px;
font-size: 12px;
font-family: 'Plus Jakarta Sans', sans-serif;
color: var(--text-mid);
background: var(--neutral-light);
outline: none;
transition: border-color .15s;
}
.sw-nonaktif-input:focus { border-color: var(--danger); box-shadow: 0 0 0 3px rgba(220,38,38,.08); }
.sw-nonaktif-input::placeholder { color: #9CA3AF; }
/* ── ACTION CELL ── */ .sw-btn-warning:hover {
.sw-action-cell { display: flex; gap: 6px; align-items: center; justify-content: center; } background: var(--warning);
color: #fff;
}
/* ── EMPTY STATE ── */ .sw-btn-success {
.sw-empty { background: var(--success-light);
padding: 60px 20px; color: var(--success);
text-align: center; border: 1px solid #A7F3D0;
color: var(--text-soft); }
}
.sw-empty-icon { font-size: 40px; margin-bottom: 12px; opacity: .4; }
.sw-empty p { font-size: 14px; margin: 0; }
/* ── DOT INDICATOR ── */ .sw-btn-success:hover {
.sw-dot { background: var(--success);
display: inline-block; color: #fff;
width: 7px; height: 7px; }
border-radius: 50%;
margin-right: 5px;
}
.sw-dot-success { background: var(--success); }
.sw-dot-danger { background: var(--danger); }
</style>
<div class="sw-page"> .sw-btn-sm {
padding: 6px 12px;
font-size: 12.5px;
border-radius: 8px;
}
{{-- TOPBAR --}} /* ── ALERT ── */
<div class="sw-topbar"> .sw-alert {
<div class="sw-topbar-left"> display: flex;
<h3>Data Siswa</h3> align-items: center;
<p>Kelola seluruh data dan status siswa</p> gap: 10px;
</div> padding: 13px 16px;
<a href="{{ route('admin.siswa.create') }}" class="sw-btn sw-btn-primary"> border-radius: var(--radius-md);
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M12 5v14M5 12h14"/></svg> font-size: 13.5px;
Tambah Siswa font-weight: 500;
</a> margin-bottom: 20px;
</div> background: var(--success-light);
color: var(--success);
border: 1px solid #A7F3D0;
}
{{-- ALERT --}} .sw-alert svg {
@if(session('success')) flex-shrink: 0;
<div class="sw-alert"> }
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
{{ session('success') }}
</div>
@endif
{{-- FILTER --}} /* ── FILTER CARD ── */
<div class="sw-card sw-filter"> .sw-card {
<form method="GET"> background: var(--surface);
<label>Kelas</label> border-radius: var(--radius-lg);
<select name="kelas_id" class="form-select"> box-shadow: var(--shadow-sm);
<option value="">Semua Kelas</option> border: 1px solid var(--border);
@foreach($kelas as $k) }
<option value="{{ $k->id }}" {{ request('kelas_id') == $k->id ? 'selected' : '' }}>
{{ $k->nama_kelas }}
</option>
@endforeach
</select>
<button type="submit" class="sw-btn sw-btn-primary">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></svg>
Filter
</button>
<a href="{{ route('admin.siswa.index') }}" class="sw-btn sw-btn-secondary">Reset</a>
</form>
</div>
{{-- BULK ACTION + TABLE --}} .sw-filter {
<form action="{{ route('admin.siswa.bulkNonaktif') }}" method="POST"> padding: 16px 20px;
@csrf margin-bottom: 18px;
}
<div class="sw-bulk-bar"> .sw-filter form {
<button type="submit" class="sw-btn sw-btn-danger" display: flex;
onclick="return confirm('Nonaktifkan siswa yang dipilih?')"> gap: 10px;
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><circle cx="12" cy="12" r="10"/><line x1="8" y1="12" x2="16" y2="12"/></svg> align-items: center;
Nonaktifkan Terpilih flex-wrap: wrap;
</button> }
</div>
<div class="sw-card sw-table-card"> .sw-filter label {
<div class="sw-table-wrap"> font-size: 13px;
<table class="sw-table"> font-weight: 600;
<thead> color: var(--text-mid);
<tr> white-space: nowrap;
<th class="center" style="width:36px"> }
<input type="checkbox" id="checkAll" class="sw-check">
</th> .sw-filter .form-select {
<th class="center" style="width:44px">No</th> border: 1px solid var(--border);
<th>Nama</th> border-radius: 8px;
<th class="center">JK</th> padding: 8px 14px;
<th class="center">NIS</th> font-size: 13px;
<th class="center">Kelas</th> color: var(--text-mid);
<th>Orang Tua</th> background: var(--neutral-light);
<th>Email</th> font-family: 'Plus Jakarta Sans', sans-serif;
<th>Alamat</th> outline: none;
<th class="center">Telepon</th> transition: border-color .15s;
<th class="center">Status</th> }
<th class="center" style="width:220px">Aksi</th>
</tr> .sw-filter .form-select:focus {
</thead> border-color: var(--primary);
<tbody> box-shadow: 0 0 0 3px rgba(79, 70, 229, .1);
@forelse($siswa as $item) }
<tr>
<td class="center"> /* ── BULK BAR ── */
<input type="checkbox" name="ids[]" value="{{ $item->id }}" class="sw-check"> .sw-bulk-bar {
</td> display: flex;
<td class="center" style="color:var(--text-soft); font-size:12px;"> align-items: center;
{{ $loop->iteration }} gap: 10px;
</td> margin-bottom: 14px;
<td class="name-cell">{{ $item->nama }}</td> }
<td class="center">
@if($item->jenis_kelamin == 'L') /* ── TABLE CARD ── */
<span class="sw-badge sw-badge-primary">L</span> .sw-table-card {
@else overflow: hidden;
<span class="sw-badge sw-badge-neutral">P</span> }
@endif
</td> .sw-table-wrap {
<td class="center" style="font-variant-numeric:tabular-nums; font-size:13px;"> overflow-x: auto;
{{ $item->nis }} }
</td>
<td class="center"> table.sw-table {
<span class="sw-badge sw-badge-info">{{ $item->kelas->nama_kelas ?? '-' }}</span> width: 100%;
</td> border-collapse: collapse;
<td>{{ $item->nama_ortu }}</td> font-size: 13.5px;
<td style="font-size:12.5px; color:var(--text-soft);">{{ $item->user->email ?? '-' }}</td> }
<td style="max-width:160px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;" title="{{ $item->alamat }}">
{{ $item->alamat }} table.sw-table thead tr {
</td> background: linear-gradient(135deg, #F5F3FF 0%, #EEF2FF 100%);
<td class="center" style="font-size:12.5px;">{{ $item->telepon }}</td> border-bottom: 2px solid #DDD6FE;
<td class="center"> }
@if($item->status == 'aktif')
<span class="sw-badge sw-badge-success"> table.sw-table thead th {
<span class="sw-dot sw-dot-success"></span>Aktif padding: 13px 16px;
</span> font-weight: 700;
@else font-size: 12px;
<span class="sw-badge sw-badge-danger"> color: #5B21B6;
<span class="sw-dot sw-dot-danger"></span>Nonaktif text-transform: uppercase;
</span> letter-spacing: .6px;
@endif white-space: nowrap;
</td> border: none;
<td> }
<div class="sw-action-cell">
<a href="{{ route('admin.siswa.edit', $item->id) }}" table.sw-table tbody tr {
class="sw-btn sw-btn-warning sw-btn-sm"> border-bottom: 1px solid var(--border);
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg> transition: background .15s;
Edit }
</a>
@if($item->status == 'aktif') table.sw-table tbody tr:last-child {
<form action="{{ route('admin.siswa.nonaktif', $item->id) }}" method="POST" class="sw-nonaktif-form"> border-bottom: none;
@csrf }
<input type="text" name="alasan" placeholder="Alasan..." required class="sw-nonaktif-input">
<button type="submit" class="sw-btn sw-btn-danger sw-btn-sm"> table.sw-table tbody tr:hover {
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><circle cx="12" cy="12" r="10"/><line x1="8" y1="12" x2="16" y2="12"/></svg> background: #FAFAFF;
Nonaktif }
</button>
</form> table.sw-table td {
@else padding: 13px 16px;
<a href="{{ route('admin.siswa.aktifkan', $item->id) }}" color: var(--text-mid);
class="sw-btn sw-btn-success sw-btn-sm"> border: none;
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><polyline points="20 6 9 17 4 12"/></svg> vertical-align: middle;
Aktifkan }
</a>
@endif table.sw-table td.center {
</div> text-align: center;
</td> }
</tr>
@empty table.sw-table td.name-cell {
<tr> font-weight: 600;
<td colspan="12"> color: var(--text-dark);
<div class="sw-empty"> }
<div class="sw-empty-icon">🎓</div>
<p>Tidak ada data siswa ditemukan</p> /* ── CUSTOM CHECKBOX ── */
</div> .sw-check {
</td> width: 16px;
</tr> height: 16px;
@endforelse accent-color: var(--primary);
</tbody> cursor: pointer;
</table> }
/* ── BADGES ── */
.sw-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
border-radius: 99px;
font-size: 11.5px;
font-weight: 600;
letter-spacing: .1px;
}
.sw-badge-success {
background: var(--success-light);
color: var(--success);
}
.sw-badge-danger {
background: var(--danger-light);
color: var(--danger);
}
.sw-badge-primary {
background: var(--primary-light);
color: var(--primary);
}
.sw-badge-neutral {
background: #F3F4F6;
color: #6B7280;
}
.sw-badge-info {
background: var(--info-light);
color: var(--info);
}
/* ── NONAKTIF INLINE FORM ── */
.sw-nonaktif-form {
display: flex;
gap: 6px;
align-items: center;
}
.sw-nonaktif-input {
width: 110px;
padding: 6px 10px;
border: 1px solid var(--border);
border-radius: 7px;
font-size: 12px;
font-family: 'Plus Jakarta Sans', sans-serif;
color: var(--text-mid);
background: var(--neutral-light);
outline: none;
transition: border-color .15s;
}
.sw-nonaktif-input:focus {
border-color: var(--danger);
box-shadow: 0 0 0 3px rgba(220, 38, 38, .08);
}
.sw-nonaktif-input::placeholder {
color: #9CA3AF;
}
/* ── ACTION CELL ── */
.sw-action-cell {
display: flex;
gap: 6px;
align-items: center;
justify-content: center;
}
/* ── EMPTY STATE ── */
.sw-empty {
padding: 60px 20px;
text-align: center;
color: var(--text-soft);
}
.sw-empty-icon {
font-size: 40px;
margin-bottom: 12px;
opacity: .4;
}
.sw-empty p {
font-size: 14px;
margin: 0;
}
/* ── DOT INDICATOR ── */
.sw-dot {
display: inline-block;
width: 7px;
height: 7px;
border-radius: 50%;
margin-right: 5px;
}
.sw-dot-success {
background: var(--success);
}
.sw-dot-danger {
background: var(--danger);
}
</style>
<div class="sw-page">
{{-- TOPBAR --}}
<div class="sw-topbar">
<div class="sw-topbar-left">
<h3>Data Siswa</h3>
<p>Kelola seluruh data dan status siswa</p>
</div> </div>
<a href="{{ route('admin.siswa.create') }}" class="sw-btn sw-btn-primary">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="M12 5v14M5 12h14" />
</svg>
Tambah Siswa
</a>
</div> </div>
</form>
</div> {{-- ALERT --}}
@if(session('success'))
<div class="sw-alert">
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
<polyline points="22 4 12 14.01 9 11.01" />
</svg>
{{ session('success') }}
</div>
@endif
<script> {{-- FILTER --}}
document.getElementById('checkAll').addEventListener('change', function () { <div class="sw-card sw-filter">
document.querySelectorAll('input[name="ids[]"]').forEach(cb => cb.checked = this.checked); <form method="GET">
}); <label>Kelas</label>
</script> <select name="kelas_id" class="form-select">
<option value="">Semua Kelas</option>
@foreach($kelas as $k)
<option value="{{ $k->id }}" {{ request('kelas_id') == $k->id ? 'selected' : '' }}>
{{ $k->nama_kelas }}
</option>
@endforeach
</select>
<button type="submit" class="sw-btn sw-btn-primary">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2">
<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3" />
</svg>
Filter
</button>
<a href="{{ route('admin.siswa.index') }}" class="sw-btn sw-btn-secondary">Reset</a>
</form>
</div>
{{-- BULK ACTION + TABLE --}}
<form action="{{ route('admin.siswa.bulkNonaktif') }}" method="POST">
@csrf
<div class="sw-bulk-bar">
<button type="submit" class="sw-btn sw-btn-danger"
onclick="return confirm('Nonaktifkan siswa yang dipilih?')">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2">
<circle cx="12" cy="12" r="10" />
<line x1="8" y1="12" x2="16" y2="12" />
</svg>
Nonaktifkan Terpilih
</button>
</div>
<div class="sw-card sw-table-card">
<div class="sw-table-wrap">
<table class="sw-table">
<thead>
<tr>
<th class="center" style="width:36px">
<input type="checkbox" id="checkAll" class="sw-check">
</th>
<th class="center" style="width:44px">No</th>
<th>Nama</th>
<th class="center">JK</th>
<th class="center">NIS</th>
<th class="center">Kelas</th>
<th>Orang Tua</th>
<th>Email</th>
<th>Alamat</th>
<th class="center">Telepon</th>
<th class="center">Status</th>
<th class="center" style="width:220px">Aksi</th>
</tr>
</thead>
<tbody>
@forelse($siswa as $item)
<tr>
<td class="center">
<input type="checkbox" name="ids[]" value="{{ $item->id }}" class="sw-check">
</td>
<td class="center" style="color:var(--text-soft); font-size:12px;">
{{ $loop->iteration }}
</td>
<td class="name-cell">{{ $item->nama }}</td>
<td class="center">
@if($item->jenis_kelamin == 'L')
<span class="sw-badge sw-badge-primary">L</span>
@else
<span class="sw-badge sw-badge-neutral">P</span>
@endif
</td>
<td class="center" style="font-variant-numeric:tabular-nums; font-size:13px;">
{{ $item->nis }}
</td>
<td class="center">
<span class="sw-badge sw-badge-info">{{ $item->kelas->nama_kelas ?? '-' }}</span>
</td>
<td>{{ $item->nama_ortu }}</td>
<td style="font-size:12.5px; color:var(--text-soft);">{{ $item->user->email ?? '-' }}</td>
<td style="max-width:160px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;"
title="{{ $item->alamat }}">
{{ $item->alamat }}
</td>
<td class="center" style="font-size:12.5px;">{{ $item->telepon }}</td>
<td class="center">
@if($item->status == 'aktif')
<span class="sw-badge sw-badge-success">
<span class="sw-dot sw-dot-success"></span>Aktif
</span>
@else
<span class="sw-badge sw-badge-danger">
<span class="sw-dot sw-dot-danger"></span>Nonaktif
</span>
@endif
</td>
<td>
<div class="sw-action-cell">
<a href="{{ route('admin.siswa.edit', $item->id) }}"
class="sw-btn sw-btn-warning sw-btn-sm">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2.2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
</svg>
Edit
</a>
<form action="{{ route('admin.siswa.destroy', $item->id) }}" method="POST"
onsubmit="return confirm('Yakin hapus data {{ $item->nama }}? Akun login juga akan terhapus!')">
@csrf
@method('DELETE')
<button type="submit" class="sw-btn sw-btn-danger sw-btn-sm">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2.2">
<polyline points="3 6 5 6 21 6" />
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
<path d="M10 11v6M14 11v6" />
<path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2" />
</svg>
Hapus
</button>
</form>
@if($item->status == 'aktif')
<form action="{{ route('admin.siswa.nonaktif', $item->id) }}" method="POST"
class="sw-nonaktif-form">
@csrf
<input type="text" name="alasan" placeholder="Alasan..." required
class="sw-nonaktif-input">
<button type="submit" class="sw-btn sw-btn-danger sw-btn-sm">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2.2">
<circle cx="12" cy="12" r="10" />
<line x1="8" y1="12" x2="16" y2="12" />
</svg>
Nonaktif
</button>
</form>
@else
<a href="{{ route('admin.siswa.aktifkan', $item->id) }}"
class="sw-btn sw-btn-success sw-btn-sm">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2.2">
<polyline points="20 6 9 17 4 12" />
</svg>
Aktifkan
</a>
@endif
</div>
</td>
</tr>
@empty
<tr>
<td colspan="12">
<div class="sw-empty">
<div class="sw-empty-icon">🎓</div>
<p>Tidak ada data siswa ditemukan</p>
</div>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
</form>
</div>
<script>
document.getElementById('checkAll').addEventListener('change', function () {
document.querySelectorAll('input[name="ids[]"]').forEach(cb => cb.checked = this.checked);
});
</script>
@endsection @endsection

View File

@ -562,63 +562,74 @@
LANGKAH 2: Login LANGKAH 2: Login
Field OTP disembunyikan otomatis untuk admin Field OTP disembunyikan otomatis untuk admin
============================================================ --}} ============================================================ --}}
<form method="POST" action="{{ route('login.otp') }}"> {{-- ============================================================
@csrf LANGKAH 2: Login
============================================================ --}}
<form method="POST" action="{{ route('login.otp') }}">
@csrf
{{-- Email --}} {{-- Tampilkan OTP jika: device baru ATAU OTP baru dikirim --}}
<div class="input-wrap"> <input type="hidden" id="has-otp-error" value="{{ (session('need_otp') || session('otp_sent')) ? '1' : '0' }}">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/>
<polyline points="22,6 12,13 2,6"/>
</svg>
<input
type="email"
name="email"
id="login-email"
placeholder="Email"
value="{{ session('email') ?? old('email') }}"
oninput="checkAdminEmail(this.value)"
required
autocomplete="email"
>
</div>
{{-- Password --}} {{-- Email --}}
<div class="input-wrap"> <div class="input-wrap">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/> <path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/>
<path d="M7 11V7a5 5 0 0 1 10 0v4"/> <polyline points="22,6 12,13 2,6"/>
</svg> </svg>
<input type="password" name="password" placeholder="Password" required autocomplete="current-password"> <input
</div> type="email"
name="email"
id="login-email"
placeholder="Email"
value="{{ old('email', session('email')) }}"
oninput="checkAdminEmail(this.value)"
required
autocomplete="email"
>
</div>
{{-- OTP disembunyikan untuk admin --}} {{-- Password --}}
<div class="input-wrap" id="wrap-otp-field"> <div class="input-wrap">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="9 11 12 14 22 4"/> <rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/> <path d="M7 11V7a5 5 0 0 1 10 0v4"/>
</svg> </svg>
<input <input type="password" name="password" placeholder="Password" required autocomplete="current-password">
type="text" </div>
name="otp"
id="otp-input"
placeholder="Masukkan OTP"
maxlength="6"
inputmode="numeric"
autocomplete="one-time-code"
>
</div>
{{-- Badge info admin --}} {{-- OTP muncul hanya jika device baru --}}
<div class="admin-info-badge" id="admin-info-badge"> <div class="input-wrap" id="wrap-otp-field" style="display:none;">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/> <polyline points="9 11 12 14 22 4"/>
</svg> <path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/>
Akun admin tidak memerlukan kode OTP </svg>
</div> <input
type="text"
name="otp"
id="otp-input"
placeholder="Masukkan OTP 6 digit"
maxlength="6"
inputmode="numeric"
autocomplete="one-time-code"
>
</div>
<button type="submit" class="btn-login">Masuk ke Portal</button> {{-- Info device baru --}}
</form> <div id="new-device-banner" style="display:none; font-size:12px; color:rgba(255,255,255,.7); background:rgba(255,255,255,.1); border:1px solid rgba(255,255,255,.2); border-radius:10px; padding:10px 13px; margin-bottom:12px;">
🔐 Perangkat baru terdeteksi. Silakan minta OTP di Langkah 1 lalu masukkan di atas.
</div>
{{-- Badge admin --}}
<div class="admin-info-badge" id="admin-info-badge">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
</svg>
Akun admin tidak memerlukan kode OTP
</div>
<button type="submit" class="btn-login">Masuk ke Portal</button>
</form>
<a href="{{ route('password.request') }}" class="forgot-link">Lupa Password?</a> <a href="{{ route('password.request') }}" class="forgot-link">Lupa Password?</a>
@ -639,33 +650,90 @@
// ================================================================ // ================================================================
// Toggle tampilan Langkah 1 (OTP section) berdasarkan role // Toggle tampilan Langkah 1 (OTP section) berdasarkan role
// ================================================================ // ================================================================
// Daftar email admin
const ADMIN_EMAILS = [
'smanbondowoso1@gmail.com',
// tambah email admin lain di sini
];
function showOtpField(show) {
const otpWrap = document.getElementById('wrap-otp-field');
const otpInput = document.getElementById('otp-input');
const banner = document.getElementById('new-device-banner');
otpWrap.style.display = show ? 'block' : 'none';
banner.style.display = show ? 'block' : 'none';
otpInput.required = show;
if (!show) otpInput.value = '';
}
function checkAdminEmail(email) { function checkAdminEmail(email) {
const isAdmin = ADMIN_EMAILS.includes(email.trim().toLowerCase()); const isAdmin = ADMIN_EMAILS.includes(email.trim().toLowerCase());
const needOtp = document.getElementById('has-otp-error').value === '1';
const step1 = document.getElementById('step1-section'); const step1 = document.getElementById('step1-section');
const adminDiv = document.getElementById('admin-divider'); const adminDiv = document.getElementById('admin-divider');
const otpWrap = document.getElementById('wrap-otp-field');
const otpInput = document.getElementById('otp-input');
const adminBadge = document.getElementById('admin-info-badge'); const adminBadge = document.getElementById('admin-info-badge');
if (isAdmin) { if (isAdmin) {
// Sembunyikan Langkah 1 dan field OTP
step1.style.display = 'none'; step1.style.display = 'none';
adminDiv.style.display = 'block'; adminDiv.style.display = 'block';
otpWrap.style.display = 'none';
adminBadge.style.display = 'flex'; adminBadge.style.display = 'flex';
otpInput.required = false; showOtpField(false);
otpInput.value = '';
} else { } else {
// Tampilkan kembali semua untuk guru/siswa
step1.style.display = 'block'; step1.style.display = 'block';
adminDiv.style.display = 'none'; adminDiv.style.display = 'none';
otpWrap.style.display = 'block';
adminBadge.style.display = 'none'; adminBadge.style.display = 'none';
otpInput.required = true; // Tampilkan OTP hanya jika server bilang device baru
showOtpField(needOtp);
} }
} }
function selectMethod(method) {
const emailBtn = document.getElementById('btn-email-method');
const waBtn = document.getElementById('btn-wa-method');
const emailWrap = document.getElementById('wrap-email-otp');
const waWrap = document.getElementById('wrap-wa-otp');
const emailInput = document.getElementById('input-email-otp');
const waInput = document.getElementById('input-wa-otp');
const label = document.getElementById('btn-otp-label');
const methodInput = document.getElementById('otp_method');
if (method === 'email') {
emailBtn.classList.add('active');
waBtn.classList.remove('active');
emailWrap.style.display = 'block';
waWrap.style.display = 'none';
emailInput.required = true;
waInput.required = false;
waInput.value = '';
label.textContent = 'Dapatkan OTP via Email';
methodInput.value = 'email';
} else {
waBtn.classList.add('active');
emailBtn.classList.remove('active');
waWrap.style.display = 'block';
emailWrap.style.display = 'none';
waInput.required = true;
emailInput.required = false;
emailInput.value = '';
label.textContent = 'Dapatkan OTP via WhatsApp';
methodInput.value = 'wa';
}
}
document.addEventListener('DOMContentLoaded', function () {
const emailEl = document.getElementById('login-email');
if (emailEl && emailEl.value.trim() !== '') {
checkAdminEmail(emailEl.value);
}
// Jika server kirim need_otp, langsung tampilkan kolom OTP
const needOtp = document.getElementById('has-otp-error').value === '1';
if (needOtp) {
showOtpField(true);
}
});
// ================================================================ // ================================================================
// Toggle metode OTP (email / WhatsApp) // Toggle metode OTP (email / WhatsApp)
// ================================================================ // ================================================================

View File

@ -366,7 +366,7 @@
<nav class="custom-topbar"> <nav class="custom-topbar">
{{-- LEFT --}} <!-- {{-- LEFT --}}
<div style="display:flex; align-items:center; gap:12px;"> <div style="display:flex; align-items:center; gap:12px;">
{{-- Mobile sidebar toggle --}} {{-- Mobile sidebar toggle --}}
<button class="tb-mobile-toggle" id="sidebarToggleTop"> <button class="tb-mobile-toggle" id="sidebarToggleTop">
@ -457,7 +457,7 @@
<a href="#" class="tb-dd-footer">Baca Semua Pesan &rarr;</a> <a href="#" class="tb-dd-footer">Baca Semua Pesan &rarr;</a>
</div> </div>
</div> </div> -->
<div class="tb-sep"></div> <div class="tb-sep"></div>

View File

@ -123,6 +123,18 @@
Route::get('/', [AdminDashboard::class, 'index']); Route::get('/', [AdminDashboard::class, 'index']);
Route::get('/dashboard', [AdminDashboard::class, 'index'])->name('dashboard'); Route::get('/dashboard', [AdminDashboard::class, 'index'])->name('dashboard');
Route::get('/profile', [ProfileController::class, 'index'])->name('profile');
Route::post('/profile', [ProfileController::class, 'update'])->name('profile.update');
Route::get('/guru/{id}/nonaktif', [GuruController::class, 'nonaktif'])->name('guru.nonaktif');
Route::get('/guru/{id}/aktifkan', [GuruController::class, 'aktifkan'])->name('guru.aktifkan');
// ✅ Route spesifik siswa HARUS di atas resource
Route::post('/siswa/bulk/nonaktif', [SiswaController::class, 'bulkNonaktif'])->name('siswa.bulkNonaktif');
Route::post('/siswa/nonaktif/{id}', [SiswaController::class, 'nonaktif'])->name('siswa.nonaktif');
Route::get('/siswa/aktifkan/{id}', [SiswaController::class, 'aktifkan'])->name('siswa.aktifkan');
// ✅ Resource SETELAH route spesifik
Route::resource('guru', GuruController::class); Route::resource('guru', GuruController::class);
Route::resource('siswa', SiswaController::class); Route::resource('siswa', SiswaController::class);
Route::resource('mapel', MataPelajaranController::class); Route::resource('mapel', MataPelajaranController::class);
@ -130,15 +142,8 @@
Route::resource('jadwal', AdminJadwal::class); Route::resource('jadwal', AdminJadwal::class);
Route::get('jadwal/grid', [AdminJadwal::class, 'grid'])->name('jadwal.grid'); Route::get('jadwal/grid', [AdminJadwal::class, 'grid'])->name('jadwal.grid');
Route::get('/profile', [ProfileController::class, 'index'])->name('profile');
Route::post('/profile', [ProfileController::class, 'update'])->name('profile.update');
Route::get('/guru/{id}/nonaktif', [GuruController::class, 'nonaktif'])->name('guru.nonaktif');
Route::get('/guru/{id}/aktifkan', [GuruController::class, 'aktifkan'])->name('guru.aktifkan');
Route::post('/siswa/bulk/nonaktif', [SiswaController::class, 'bulkNonaktif'])->name('siswa.bulkNonaktif');
}); });
// Siswa nonaktif/aktifkan // Siswa nonaktif/aktifkan
Route::prefix('admin')->name('admin.')->group(function () { Route::prefix('admin')->name('admin.')->group(function () {