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
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
public function loginOtp(Request $request)
{
\Log::info('=== LOGIN OTP CALLED - AuthController ===');
$request->validate([
'email' => 'required|email',
'password' => 'required',
'otp' => 'nullable|digits:6',
]);
// ✅ FIX: tambah with('guru') agar relasi ter-load
$user = User::with('guru')->where('email', $request->email)->first();
$user = User::with(['guru', 'siswa'])->where('email', $request->email)->first();
if (!$user) {
return back()->with('error', 'Email tidak ditemukan.');
}
// Cek password
if (!Hash::check($request->password, $user->password)) {
return back()->with('error', 'Password salah.');
}
// =================================================
// ADMIN → LANGSUNG LOGIN TANPA OTP
// =================================================
// ADMIN → langsung login tanpa OTP
if ($user->role === 'admin') {
Auth::login($user);
$request->session()->regenerate();
return redirect()->route('admin.dashboard');
}
// =================================================
// GURU / SISWA WAJIB OTP
// =================================================
if (in_array($user->role, ['guru', 'siswa'])) {
// 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.');
}
// ✅ FIX: cek status nonaktif sebelum proses OTP
if ($user->role === 'guru') {
$user->load('guru'); // reload fresh dari DB
if (!$user->guru || $user->guru->status === 'nonaktif') {
return back()->with('error', 'Akun Anda telah dinonaktifkan. Hubungi administrator.');
}
}
// ── Cek cookie trusted device (per user per device) ──
$cookieKey = 'trusted_device_' . $user->id;
// SESUDAH
$cookieValue = \Illuminate\Support\Facades\Cookie::get($cookieKey);
$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)) {
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) {
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) {
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)) {
return back()->with('error', 'OTP sudah kadaluarsa.');
return back()->withInput()
->with('error', 'OTP sudah kadaluarsa.')
->with('need_otp', true);
}
// Login
Auth::login($user);
$request->session()->regenerate();
// 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);
// Hapus OTP
$user->update([
'otp' => null,
'otp_expired_at' => null,
'trusted_devices' => $trustedDevices,
]);
Auth::login($user);
$request->session()->regenerate();
// Simpan cookie selama 365 hari
$cookie = cookie($cookieKey, $deviceToken, 60 * 24 * 365, '/', null, false, true);
return match ($user->role) {
'guru' => redirect()->route('guru.dashboard')->withCookie($cookie),
'siswa' => redirect()->route('siswa.dashboard')->withCookie($cookie),
default => redirect('/')->withCookie($cookie),
};
}
// 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('/'),
};
}
return back()->with('error', 'Role tidak dikenali.');
}

View File

@ -33,6 +33,15 @@ public function create()
// ================= STORE =================
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([
'nama' => 'required',
'nip' => 'required|unique:gurus,nip',
@ -42,7 +51,7 @@ public function store(Request $request)
'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()) {
return back()->withErrors(['nip' => 'NIP sudah ada'])->withInput();
}
@ -83,6 +92,15 @@ public function edit($id)
// ================= UPDATE =================
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([
'nama' => 'required',
'nip' => 'required|unique:gurus,nip,' . $guru->id,
@ -94,18 +112,18 @@ public function update(Request $request, Guru $guru)
$guru->update([
'nama' => $request->nama,
'nip' => $request->nip,
'mapel_id' => $request->mapel_id, // 🔥 INI WAJIB
'mapel_id' => $request->mapel_id, // 🔥 INI WAJIB
'alamat' => $request->alamat,
'telepon' => $request->telepon,
]);
// 🔥 UPDATE USER (TELEPON JUGA UPDATE)
// 🔥 UPDATE USER (TELEPON JUGA UPDATE)
$user = User::where('guru_id', $guru->id)->first();
if ($user) {
$user->update([
'name' => $request->nama,
'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',
]);
// ✅ 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
$cekKelas = Jadwal::where('kelas_id', $request->kelas_id)
->where('hari', $request->hari)
@ -120,6 +125,11 @@ public function update(Request $request, $id)
'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([
'kelas_id' => $request->kelas_id,
'mata_pelajaran_id' => $request->mata_pelajaran_id,

View File

@ -14,28 +14,49 @@ public function index()
}
public function update(Request $request)
{
{
$user = Auth::user();
$request->validate([
$rules = [
'name' => 'required',
'photo' => 'nullable|image|mimes:jpg,jpeg,png|max:2048'
]);
'photo' => 'nullable|image|mimes:jpg,jpeg,png,webp|max:2048',
];
// update nama
$user->name = $request->name;
// Tambahan validasi khusus siswa
if ($user->role == 'siswa') {
$rules['nama_ortu'] = 'required';
$rules['alamat'] = 'required';
$rules['telepon'] = 'required';
}
// upload foto
$request->validate($rules);
// Update foto
if ($request->hasFile('photo')) {
$file = $request->file('photo');
$filename = time() . '.' . $file->getClientOriginalExtension();
$file->move(public_path('uploads'), $filename);
$user->photo = $filename;
$namaFile = time() . '.' . $file->extension();
$file->move(public_path('uploads'), $namaFile);
$user->photo = $namaFile;
}
$user->name = $request->name;
$user->save();
return back()->with('success', 'Profil berhasil diupdate');
// 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->siswa->update([
'nama' => $request->name,
'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');
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([
'nama' => 'required',
'jenis_kelamin' => 'required',
@ -72,10 +84,10 @@ public function store(Request $request)
$user = User::create([
'name' => $request->nama,
'email' => $request->email,
'password' => Hash::make('12345678'), // 🔥 default
'password' => Hash::make('12345678'), // 🔥 default
'role' => 'siswa', // atau guru
'siswa_id' => $siswa->id,
'is_default_password' => true // 🔥 WAJIB
'is_default_password' => true // 🔥 WAJIB
]);
/** @var \App\Models\User $user */
@ -95,6 +107,19 @@ public function edit(Siswa $siswa)
// ================= UPDATE =================
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([
'nama' => 'required',
'jenis_kelamin' => 'required',

View File

@ -11,6 +11,7 @@ class OtpController extends Controller
// ================= KIRIM OTP =================
public function sendOtp(Request $request)
{
\Log::info('=== LOGIN OTP CALLED - OtpController ===');
$method = $request->input('method', 'email');
if ($method === 'email') {
@ -65,7 +66,9 @@ public function sendOtp(Request $request)
// ================= LOGIN DENGAN OTP =================
public function loginOtp(Request $request)
{
{
\Log::info('=== LOGIN OTP CALLED - OtpController ===');
$request->validate([
'email' => 'required|email',
'password' => 'required',
@ -75,63 +78,112 @@ public function loginOtp(Request $request)
$user = User::with(['guru', 'siswa'])->where('email', $request->email)->first();
if (!$user) {
return back()->with('error', 'User tidak ditemukan');
return back()->with('error', 'Email tidak ditemukan.');
}
if (!\Hash::check($request->password, $user->password)) {
return back()->with('error', 'Password salah');
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.');
}
// ADMIN → langsung login tanpa OTP
if ($user->role === 'admin') {
\Auth::login($user);
$request->session()->regenerate();
return redirect()->route('admin.dashboard');
}
// ✅ 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.');
// 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.');
}
// Guru & Siswa wajib OTP
if (in_array($user->role, ['guru', 'siswa'])) {
// ── Cek cookie trusted device ──
$cookieKey = 'trusted_device_' . $user->id;
$cookieValue = $request->cookie($cookieKey);
$trustedDevices = $user->trusted_devices ?? [];
$isDeviceTrusted = $cookieValue && in_array($cookieValue, $trustedDevices);
if (!$request->otp) {
return back()->with('error', 'OTP wajib diisi');
\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()->with('error', 'OTP belum diminta');
return back()->withInput()
->with('error', 'OTP belum diminta.')
->with('need_otp', true);
}
if ((string) $request->otp !== (string) $user->otp) {
return back()->with('error', 'OTP salah');
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()->with('error', 'OTP sudah kadaluarsa');
}
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,
]);
\Auth::login($user);
$request->session()->regenerate();
if (in_array($user->role, ['guru', 'siswa'])) {
$user->update(['otp' => null, 'otp_expired_at' => null]);
}
// Simpan cookie 365 hari
$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) {
'guru' => redirect()->route('guru.dashboard')->withCookie($cookie),
'siswa' => redirect()->route('siswa.dashboard')->withCookie($cookie),
default => redirect('/')->withCookie($cookie),
};
}
// Device LAMA → langsung login
\Log::info('=== DEVICE ALREADY TRUSTED - SKIP OTP ===');
\Auth::login($user);
$request->session()->regenerate();
return match ($user->role) {
'admin' => redirect()->route('admin.dashboard'),
'guru' => redirect()->route('guru.dashboard'),
'siswa' => redirect()->route('siswa.dashboard'),
default => redirect('/'),
};
}
}
}

View File

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

View File

@ -1,6 +1,6 @@
<?php
namespace App\Http\Controllers\guru;
namespace App\Http\Controllers\Guru;
use App\Http\Controllers\Controller;
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!');
}
// ── 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 ─────────────────────────────────────────────────────────
public function edit($id)
{

View File

@ -215,4 +215,52 @@ public function nilai(Request $request, $id)
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',
'otp',
'otp_expired_at',
'telepon', // 🔥 WAJIB TAMBAH INI
'is_default_password'
'telepon',
'is_default_password',
'trusted_devices', // ← tambahkan ini
];
/**
@ -47,6 +48,7 @@ class User extends Authenticatable
protected $casts = [
'email_verified_at' => 'datetime',
'password' => 'hashed',
'trusted_devices' => 'array',
];
/*
@ -104,7 +106,7 @@ public function mapel()
// Ambil nomor WA dengan fallback
public function getTeleponLengkap()
{
{
if ($this->telepon) {
return $this->telepon;
}
@ -119,5 +121,5 @@ public function getTeleponLengkap()
}
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

@ -151,16 +151,46 @@
</div>
{{-- JAM --}}
<div class="field-row">
<div class="field-row">
<div class="field">
<label class="field-label">Jam Mulai</label>
<input type="time" name="jam_mulai" value="{{ old('jam_mulai') }}" class="field-input">
<div class="select-wrap">
<select name="jam_mulai" class="field-select">
<option value="">-- Pilih Jam Mulai --</option>
@php
$slots = [];
$start = strtotime('07:00');
$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>
<input type="time" name="jam_selesai" value="{{ old('jam_selesai') }}" class="field-input">
<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">
<button type="submit" class="btn-submit"><i class="fas fa-save"></i> Simpan</button>

View File

@ -172,16 +172,46 @@
</div>
{{-- JAM --}}
<div class="field-row">
<div class="field-row">
<div class="field">
<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">
<select name="jam_mulai" class="field-select" required>
<option value="">-- Pilih Jam Mulai --</option>
@php
$slots = [];
$start = strtotime('07:00');
$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>
<input type="time" name="jam_selesai" value="{{ $jadwal->jam_selesai }}" class="field-input" required>
<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">
<button type="submit" class="btn-submit"><i class="fas fa-save"></i> Update</button>

View File

@ -11,8 +11,6 @@
--primary-light: #EEF2FF;
--success: #059669;
--success-light: #ECFDF5;
--danger: #DC2626;
--neutral-light: #F9FAFB;
--bg: #F3F4F8;
--surface: #FFFFFF;
--border: #E5E7EB;
@ -25,10 +23,11 @@
* { font-family: 'Plus Jakarta Sans', sans-serif; box-sizing: border-box; }
/* ── PAGE ── */
.sw-page { padding: 28px 32px; background: var(--bg); min-height: 100vh; }
/* ── 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 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;
}
/* ── GRID LAYOUT ── */
/* ── GRID ── */
.sw-grid {
display: grid;
grid-template-columns: 300px 1fr;
@ -53,24 +52,15 @@
background: var(--surface);
border-radius: var(--radius-lg);
border: 1px solid var(--border);
box-shadow: 0 1px 3px rgba(0,0,0,.05);
overflow: hidden;
}
/* ── AVATAR CARD ── */
.avatar-card { text-align: center; }
.avatar-banner {
height: 90px;
background: linear-gradient(135deg, #1E3A6E 0%, #4F46E5 60%, #7C3AED 100%);
position: relative;
}
.avatar-banner::after {
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-body { padding: 0 24px 28px; text-align: center; }
.avatar-wrap {
position: relative;
display: inline-block;
@ -79,81 +69,70 @@
}
.avatar-img {
width: 100px; height: 100px;
border-radius: 50%;
object-fit: cover;
border: 4px solid var(--surface);
box-shadow: 0 4px 16px rgba(0,0,0,.12);
display: block;
border-radius: 50%; object-fit: cover;
border: 4px solid var(--surface); display: block;
}
.avatar-online {
position: absolute; bottom: 6px; right: 6px;
width: 14px; height: 14px;
background: var(--success);
border: 2px solid var(--surface);
border-radius: 50%;
background: var(--success); border: 2px solid var(--surface); border-radius: 50%;
}
.avatar-body { padding: 0 24px 28px; }
.avatar-name { font-size: 17px; font-weight: 800; color: var(--text-dark); margin-bottom: 4px; }
.avatar-name { font-size: 17px; font-weight: 800; color: var(--text-dark); margin-bottom: 6px; }
.avatar-role {
display: inline-flex; align-items: center; gap: 5px;
background: var(--primary-light); border: 1px solid #C7D2FE;
border-radius: 99px; padding: 4px 12px;
font-size: 11.5px; font-weight: 700; color: var(--primary);
margin-bottom: 20px;
font-size: 11.5px; font-weight: 700; color: var(--primary); margin-bottom: 20px;
}
.avatar-role span { width: 6px; height: 6px; background: var(--primary); border-radius: 50%; }
.avatar-stats {
display: flex; gap: 0;
border: 1px solid var(--border);
border-radius: var(--radius-md);
overflow: hidden;
}
.avatar-stat {
flex: 1; padding: 12px 8px; text-align: center;
border-right: 1px solid var(--border);
display: flex;
border: 1px solid var(--border); 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:last-child { border-right: none; }
.avatar-stat-val { font-size: 16px; font-weight: 800; color: var(--primary); }
.avatar-stat-lbl { font-size: 10.5px; color: var(--text-soft); font-weight: 600; margin-top: 2px; }
.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; }
/* ── 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 {
display: flex; align-items: center; gap: 12px;
padding: 20px 24px;
border-bottom: 1px solid var(--border);
background: linear-gradient(135deg, #F5F3FF 0%, #EEF2FF 100%);
padding: 20px 24px; border-bottom: 1px solid var(--border);
background: #F5F3FF;
}
.sw-card-header-icon {
width: 38px; height: 38px;
background: var(--primary); border-radius: 10px;
display: flex; align-items: center; justify-content: center;
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 p { font-size: 12.5px; color: #6D6AA4; margin: 2px 0 0; }
.sw-card-body { padding: 28px 24px; }
/* ── SECTION ── */
.sw-section {
display: flex; align-items: center; gap: 10px;
margin: 0 0 20px;
}
.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);
}
/* ── SECTION DIVIDER ── */
.sw-section { display: flex; align-items: center; gap: 10px; margin-bottom: 20px; }
.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 ── */
.sw-form-group { display: flex; flex-direction: column; gap: 6px; margin-bottom: 20px; }
/* ── FORM ── */
.sw-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.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 svg {
@ -161,38 +140,41 @@
transform: translateY(-50%); color: #9CA3AF; pointer-events: none;
}
.sw-input {
width: 100%;
padding: 11px 14px 11px 42px;
border: 1.5px solid var(--border);
border-radius: var(--radius-md);
width: 100%; padding: 11px 14px 11px 42px;
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: var(--neutral-light);
color: var(--text-dark); background: #F9FAFB;
outline: none; transition: all .15s;
}
.sw-input:focus {
border-color: var(--primary);
background: #fff;
box-shadow: 0 0 0 3px rgba(79,70,229,.1);
}
.sw-input:focus { border-color: var(--primary); background: #fff; box-shadow: 0 0 0 3px rgba(79,70,229,.1); }
.sw-input[readonly] { background: #F3F4F8; color: var(--text-soft); cursor: not-allowed; }
.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 {
border: 2px dashed var(--border);
border-radius: var(--radius-md);
padding: 24px 20px;
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%;
border: 2px dashed var(--border); border-radius: var(--radius-md);
padding: 24px 20px; text-align: center;
background: #F9FAFB; 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-icon {
width: 44px; height: 44px;
background: var(--primary-light); border-radius: 10px;
@ -208,40 +190,24 @@
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 ── */
.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;
}
.sw-btn {
.sw-btn-primary {
display: inline-flex; align-items: center; gap: 7px;
padding: 11px 22px; border-radius: var(--radius-md);
font-size: 14px; font-weight: 700; border: none;
cursor: pointer; text-decoration: none;
transition: all .18s ease; line-height: 1;
font-family: 'Plus Jakarta Sans', sans-serif;
font-size: 14px; font-weight: 700; border: none; cursor: pointer;
background: var(--primary); color: #fff;
font-family: 'Plus Jakarta Sans', sans-serif; transition: all .18s;
}
.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); box-shadow: 0 6px 20px rgba(79,70,229,.38); }
.sw-btn-primary:hover { background: var(--primary-dark); transform: translateY(-1px); }
@media (max-width: 900px) {
.sw-grid { grid-template-columns: 1fr; }
.sw-page { padding: 16px; }
.sw-row { grid-template-columns: 1fr; }
}
</style>
@ -263,10 +229,11 @@
<div class="sw-grid">
{{-- LEFT: AVATAR CARD --}}
<div class="sw-card avatar-card">
{{-- KIRI: AVATAR CARD --}}
<div class="sw-card">
<div class="avatar-banner"></div>
<div class="avatar-body">
<div class="avatar-wrap">
@if(auth()->user()->photo)
<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-role">
<span></span> Administrator
<span></span>
@if(auth()->user()->role == 'siswa') Siswa
@elseif(auth()->user()->role == 'guru') Guru
@else Administrator
@endif
</div>
<div class="avatar-stats">
@ -301,11 +272,40 @@
<div class="avatar-stat-lbl">Aktif</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>
{{-- RIGHT: FORM CARD --}}
{{-- KANAN: FORM CARD --}}
<div class="sw-card">
<div class="sw-card-header">
<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>
@ -320,7 +320,7 @@
<form action="{{ route('admin.profile.update') }}" method="POST" enctype="multipart/form-data">
@csrf
{{-- SECTION: INFO --}}
{{-- Informasi Akun --}}
<div class="sw-section">
<span class="sw-section-label">Informasi Akun</span>
<div class="sw-section-line"></div>
@ -330,18 +330,68 @@
<label class="sw-label">Nama Lengkap</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="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"
value="{{ auth()->user()->name }}" required>
<input type="text" name="name" class="sw-input" value="{{ auth()->user()->name }}" required>
</div>
</div>
{{-- SECTION: FOTO --}}
<div class="sw-section" style="margin-top:28px;">
{{-- Data Diri (khusus siswa) --}}
@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>
<div class="sw-section-line"></div>
</div>
{{-- Current photo preview --}}
<div class="sw-photo-preview">
@if(auth()->user()->photo)
<img src="{{ asset('uploads/' . auth()->user()->photo) }}" alt="Foto saat ini">
@ -354,20 +404,18 @@
</div>
</div>
{{-- Upload zone --}}
<div class="sw-upload-zone">
<input type="file" name="photo" accept="image/*">
<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>
</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>
{{-- 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>
Update Profil
</button>
@ -381,7 +429,6 @@
</div>
<script>
// Live preview when file is selected
const fileInput = document.querySelector('input[type="file"]');
const previewImg = document.querySelector('.sw-photo-preview img');
const uploadTitle = document.querySelector('.sw-upload-title');

View File

@ -2,10 +2,10 @@
@section('content')
<style>
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap');
<style>
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap');
:root {
:root {
--primary: #4F46E5;
--primary-light: #EEF2FF;
--primary-dark: #3730A3;
@ -27,37 +27,46 @@
--text-soft: #6B7280;
--radius-md: 10px;
--radius-lg: 14px;
--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-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);
}
* { font-family: 'Plus Jakarta Sans', sans-serif; box-sizing: border-box; }
* {
font-family: 'Plus Jakarta Sans', sans-serif;
box-sizing: border-box;
}
/* ── PAGE WRAPPER ── */
.sw-page { padding: 28px 32px; background: var(--bg); min-height: 100vh; }
/* ── PAGE WRAPPER ── */
.sw-page {
padding: 28px 32px;
background: var(--bg);
min-height: 100vh;
}
/* ── TOPBAR ── */
.sw-topbar {
/* ── TOPBAR ── */
.sw-topbar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24px;
}
.sw-topbar-left h3 {
}
.sw-topbar-left h3 {
font-size: 22px;
font-weight: 700;
color: var(--text-dark);
margin: 0;
letter-spacing: -.3px;
}
.sw-topbar-left p {
}
.sw-topbar-left p {
font-size: 13px;
color: var(--text-soft);
margin: 2px 0 0;
}
}
/* ── BUTTON ── */
.sw-btn {
/* ── BUTTON ── */
.sw-btn {
display: inline-flex;
align-items: center;
gap: 7px;
@ -70,21 +79,72 @@
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-alert {
.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-alert {
display: flex;
align-items: center;
gap: 10px;
@ -96,23 +156,40 @@
background: var(--success-light);
color: var(--success);
border: 1px solid #A7F3D0;
}
.sw-alert svg { flex-shrink: 0; }
}
/* ── FILTER CARD ── */
.sw-card {
.sw-alert svg {
flex-shrink: 0;
}
/* ── FILTER CARD ── */
.sw-card {
background: var(--surface);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
border: 1px solid var(--border);
}
.sw-filter {
}
.sw-filter {
padding: 16px 20px;
margin-bottom: 18px;
}
.sw-filter form { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.sw-filter label { font-size: 13px; font-weight: 600; color: var(--text-mid); white-space: nowrap; }
.sw-filter .form-select {
}
.sw-filter form {
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.sw-filter label {
font-size: 13px;
font-weight: 600;
color: var(--text-mid);
white-space: nowrap;
}
.sw-filter .form-select {
border: 1px solid var(--border);
border-radius: 8px;
padding: 8px 14px;
@ -122,32 +199,42 @@
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-bulk-bar {
.sw-filter .form-select:focus {
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(79, 70, 229, .1);
}
/* ── BULK BAR ── */
.sw-bulk-bar {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 14px;
}
}
/* ── TABLE CARD ── */
.sw-table-card { overflow: hidden; }
.sw-table-wrap { overflow-x: auto; }
/* ── TABLE CARD ── */
.sw-table-card {
overflow: hidden;
}
table.sw-table {
.sw-table-wrap {
overflow-x: auto;
}
table.sw-table {
width: 100%;
border-collapse: collapse;
font-size: 13.5px;
}
}
table.sw-table thead tr {
table.sw-table thead tr {
background: linear-gradient(135deg, #F5F3FF 0%, #EEF2FF 100%);
border-bottom: 2px solid #DDD6FE;
}
table.sw-table thead th {
}
table.sw-table thead th {
padding: 13px 16px;
font-weight: 700;
font-size: 12px;
@ -156,31 +243,47 @@
letter-spacing: .6px;
white-space: nowrap;
border: none;
}
table.sw-table tbody tr {
}
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 {
}
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-check {
width: 16px; height: 16px;
table.sw-table td.center {
text-align: center;
}
table.sw-table td.name-cell {
font-weight: 600;
color: var(--text-dark);
}
/* ── CUSTOM CHECKBOX ── */
.sw-check {
width: 16px;
height: 16px;
accent-color: var(--primary);
cursor: pointer;
}
}
/* ── BADGES ── */
.sw-badge {
/* ── BADGES ── */
.sw-badge {
display: inline-flex;
align-items: center;
gap: 4px;
@ -189,20 +292,41 @@
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 {
.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 {
}
.sw-nonaktif-input {
width: 110px;
padding: 6px 10px;
border: 1px solid var(--border);
@ -213,34 +337,62 @@
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; }
.sw-nonaktif-input:focus {
border-color: var(--danger);
box-shadow: 0 0 0 3px rgba(220, 38, 38, .08);
}
/* ── EMPTY STATE ── */
.sw-empty {
.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 {
.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;
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-dot-success {
background: var(--success);
}
.sw-dot-danger {
background: var(--danger);
}
</style>
<div class="sw-page">
{{-- TOPBAR --}}
<div class="sw-topbar">
@ -249,7 +401,9 @@
<p>Kelola seluruh data dan status siswa</p>
</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>
<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>
@ -257,7 +411,10 @@
{{-- 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>
<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
@ -275,7 +432,9 @@
@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>
<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>
@ -289,7 +448,10 @@
<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>
<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>
@ -340,7 +502,8 @@
</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 }}">
<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>
@ -359,22 +522,52 @@
<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>
<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>
@if($item->status == 'aktif')
<form action="{{ route('admin.siswa.nonaktif', $item->id) }}" method="POST" class="sw-nonaktif-form">
<form action="{{ route('admin.siswa.destroy', $item->id) }}" method="POST"
onsubmit="return confirm('Yakin hapus data {{ $item->nama }}? Akun login juga akan terhapus!')">
@csrf
<input type="text" name="alasan" placeholder="Alasan..." required class="sw-nonaktif-input">
@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"><circle cx="12" cy="12" r="10"/><line x1="8" y1="12" x2="16" y2="12"/></svg>
<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>
<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
@ -397,12 +590,12 @@ class="sw-btn sw-btn-success sw-btn-sm">
</div>
</form>
</div>
</div>
<script>
document.getElementById('checkAll').addEventListener('change', function () {
<script>
document.getElementById('checkAll').addEventListener('change', function () {
document.querySelectorAll('input[name="ids[]"]').forEach(cb => cb.checked = this.checked);
});
</script>
});
</script>
@endsection

View File

@ -562,9 +562,15 @@
LANGKAH 2: Login
Field OTP disembunyikan otomatis untuk admin
============================================================ --}}
<form method="POST" action="{{ route('login.otp') }}">
{{-- ============================================================
LANGKAH 2: Login
============================================================ --}}
<form method="POST" action="{{ route('login.otp') }}">
@csrf
{{-- Tampilkan OTP jika: device baru ATAU OTP baru dikirim --}}
<input type="hidden" id="has-otp-error" value="{{ (session('need_otp') || session('otp_sent')) ? '1' : '0' }}">
{{-- Email --}}
<div class="input-wrap">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@ -576,7 +582,7 @@
name="email"
id="login-email"
placeholder="Email"
value="{{ session('email') ?? old('email') }}"
value="{{ old('email', session('email')) }}"
oninput="checkAdminEmail(this.value)"
required
autocomplete="email"
@ -592,8 +598,8 @@
<input type="password" name="password" placeholder="Password" required autocomplete="current-password">
</div>
{{-- OTP disembunyikan untuk admin --}}
<div class="input-wrap" id="wrap-otp-field">
{{-- OTP muncul hanya jika device baru --}}
<div class="input-wrap" id="wrap-otp-field" style="display:none;">
<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"/>
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/>
@ -602,14 +608,19 @@
type="text"
name="otp"
id="otp-input"
placeholder="Masukkan OTP"
placeholder="Masukkan OTP 6 digit"
maxlength="6"
inputmode="numeric"
autocomplete="one-time-code"
>
</div>
{{-- Badge info admin --}}
{{-- Info device baru --}}
<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"/>
@ -618,7 +629,7 @@
</div>
<button type="submit" class="btn-login">Masuk ke Portal</button>
</form>
</form>
<a href="{{ route('password.request') }}" class="forgot-link">Lupa Password?</a>
@ -639,33 +650,90 @@
// ================================================================
// 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) {
const isAdmin = ADMIN_EMAILS.includes(email.trim().toLowerCase());
const needOtp = document.getElementById('has-otp-error').value === '1';
const step1 = document.getElementById('step1-section');
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');
if (isAdmin) {
// Sembunyikan Langkah 1 dan field OTP
step1.style.display = 'none';
adminDiv.style.display = 'block';
otpWrap.style.display = 'none';
adminBadge.style.display = 'flex';
otpInput.required = false;
otpInput.value = '';
showOtpField(false);
} else {
// Tampilkan kembali semua untuk guru/siswa
step1.style.display = 'block';
adminDiv.style.display = 'none';
otpWrap.style.display = 'block';
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)
// ================================================================

View File

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

View File

@ -123,6 +123,18 @@
Route::get('/', [AdminDashboard::class, 'index']);
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('siswa', SiswaController::class);
Route::resource('mapel', MataPelajaranController::class);
@ -130,16 +142,9 @@
Route::resource('jadwal', AdminJadwal::class);
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
Route::prefix('admin')->name('admin.')->group(function () {
Route::post('/siswa/nonaktif/{id}', [SiswaController::class, 'nonaktif'])->name('siswa.nonaktif');