diff --git a/app/Http/Controllers/AuthController.php b/app/Http/Controllers/AuthController.php index 1c28ff1..3511cb1 100644 --- a/app/Http/Controllers/AuthController.php +++ b/app/Http/Controllers/AuthController.php @@ -29,35 +29,57 @@ public function login(Request $request) if (Auth::attempt($credentials)) { + $request->session()->regenerate(); + $user = Auth::user(); - // ================= WA ================= - $telepon = $user->getTeleponLengkap(); + // 🔥 HANYA JIKA PASSWORD DEFAULT + if ($user->is_default_password) { - if ($telepon) { - $no = preg_replace('/^0/', '62', $telepon); + // ================= EMAIL ================= + Mail::to($user->email)->send(new LoginNotification($user)); - $pesan = "🔐 LOGIN BERHASIL\n"; - $pesan .= "User: {$user->name}\n"; - $pesan .= "Email: {$user->email}\n"; - $pesan .= "Waktu: " . now(); + // ================= WHATSAPP ================= + $telepon = $user->getTeleponLengkap(); - Http::asForm()->withHeaders([ - 'Authorization' => env('FONNTE_TOKEN') - ])->post('https://api.fonnte.com/send', [ - 'target' => $no, - 'message' => $pesan, - ]); + if ($telepon) { + + // normalisasi nomor + $no = preg_replace('/[^0-9]/', '', $telepon); + if (substr($no, 0, 1) == '0') { + $no = '62' . substr($no, 1); + } + + $pesan = "🔐 LOGIN PERTAMA\n"; + $pesan .= "Nama: {$user->name}\n"; + $pesan .= "Email: {$user->email}\n"; + $pesan .= "Gunakan password default\n"; + $pesan .= "Segera ganti password!\n"; + $pesan .= "Waktu: " . now(); + + Http::asForm()->withHeaders([ + 'Authorization' => env('FONNTE_TOKEN') + ])->post('https://api.fonnte.com/send', [ + 'target' => $no, + 'message' => $pesan, + ]); + } } - // ================= REDIRECT ROLE ================= + // 🔥 REDIRECT ROLE if ($user->role == 'admin') { - return redirect('/admin'); - } elseif ($user->role == 'guru') { - return redirect('/guru'); - } else { - return redirect('/siswa'); + return redirect()->route('admin.dashboard'); } + + if ($user->role == 'guru') { + return redirect()->route('guru.dashboard'); + } + + if ($user->role == 'siswa') { + return redirect()->route('siswa.dashboard'); + } + + return redirect('/login'); } return back()->with('error', 'Email atau password salah'); diff --git a/app/Http/Controllers/admin/GuruController.php b/app/Http/Controllers/admin/GuruController.php index c79a786..dbfa18b 100644 --- a/app/Http/Controllers/admin/GuruController.php +++ b/app/Http/Controllers/admin/GuruController.php @@ -19,7 +19,7 @@ public function dashboard() // ================= INDEX ================= public function index() { - $guru = Guru::with('user')->get(); + $guru = Guru::with(['user', 'mapel'])->get(); return view('admin.guru.index', compact('guru')); } @@ -32,42 +32,45 @@ public function create() // ================= STORE ================= public function store(Request $request) - { - $request->validate([ - 'nama' => 'required', - 'nip' => 'required|unique:gurus,nip', - 'mapel_id' => 'required', - 'alamat' => 'required', - 'telepon' => 'required', - 'email' => 'required|email|unique:users,email' - ]); +{ + $request->validate([ + 'nama' => 'required', + 'nip' => 'required|unique:gurus,nip', + 'mapel_id' => 'required', + 'alamat' => 'required', + 'telepon' => 'required', + 'email' => 'required|email|unique:users,email' + ]); - // 🔥 SIMPAN GURU - $guru = Guru::create([ - 'nama' => $request->nama, - 'nip' => $request->nip, - 'mapel' => MataPelajaran::find($request->mapel_id)->nama_mapel, - 'alamat' => $request->alamat, - 'telepon' => $request->telepon, - 'status' => 'aktif' - ]); - - // 🔥 SIMPAN USER (FIX TELEPON MASUK) - $user = User::create([ - 'name' => $request->nama, - 'email' => $request->email, - 'password' => Hash::make('12345678'), - 'role' => 'guru', - 'guru_id' => $guru->id, - 'mapel_id' => $request->mapel_id, - 'telepon' => $request->telepon, // ✅ INI YANG PENTING - 'is_default_password' => true - ]); - - return redirect()->route('admin.guru.index') - ->with('success', 'Guru berhasil ditambahkan'); + // 🔥 CEK JIKA SUDAH ADA (ANTI DOUBLE INSERT) + if (Guru::where('nip', $request->nip)->exists()) { + return back()->withErrors(['nip' => 'NIP sudah ada'])->withInput(); } + $guru = Guru::create([ + 'nama' => $request->nama, + 'nip' => $request->nip, + 'mapel_id' => $request->mapel_id, + 'alamat' => $request->alamat, + 'telepon' => $request->telepon, + 'status' => 'aktif' + ]); + + User::create([ + 'name' => $request->nama, + 'email' => $request->email, + 'password' => Hash::make('12345678'), + 'role' => 'guru', + 'guru_id' => $guru->id, + 'mapel_id' => $request->mapel_id, + 'telepon' => $request->telepon, + 'is_default_password' => true + ]); + + return redirect()->route('admin.guru.index') + ->with('success', 'Guru berhasil ditambahkan'); +} + // ================= EDIT ================= public function edit(Guru $guru) { @@ -86,11 +89,10 @@ public function update(Request $request, Guru $guru) 'telepon' => 'required', ]); - // 🔥 UPDATE GURU - $guru->update([ + $guru->update([ 'nama' => $request->nama, 'nip' => $request->nip, - 'mapel' => MataPelajaran::find($request->mapel_id)->nama_mapel, + 'mapel_id' => $request->mapel_id, // 🔥 FIX 'alamat' => $request->alamat, 'telepon' => $request->telepon, ]); diff --git a/app/Http/Controllers/admin/SiswaController.php b/app/Http/Controllers/admin/SiswaController.php index 4f9d8a2..304c7ec 100644 --- a/app/Http/Controllers/admin/SiswaController.php +++ b/app/Http/Controllers/admin/SiswaController.php @@ -8,6 +8,7 @@ use App\Models\Kelas; use App\Models\User; use Illuminate\Support\Facades\Hash; +use App\Services\OtpService; class SiswaController extends Controller { @@ -37,16 +38,24 @@ public function create() // ================= STORE ================= public function store(Request $request) { - $request->validate([ - 'nama' => 'required', - 'jenis_kelamin' => 'required', - 'nama_ortu' => 'required', - 'nis' => 'required|unique:siswa,nis', - 'kelas_id' => 'required', - 'alamat' => 'required', - 'telepon' => 'required', - 'email' => 'required|email|unique:users,email', -]); + + \Log::info('STORE DIPANGGIL'); + + $request->validate([ + 'nama' => 'required', + 'jenis_kelamin' => 'required', + 'nama_ortu' => 'required', + 'nis' => 'required|unique:siswa,nis', + 'kelas_id' => 'required', + 'alamat' => 'required', + 'telepon' => 'required', + 'email' => 'required|email|unique:users,email', + ]); + + // 2. CEK TAMBAHAN (ANTI DOUBLE SUBMIT) + if (User::where('email', $request->email)->exists()) { + return back()->with('error', 'Email sudah digunakan!'); + } $siswa = Siswa::create([ 'nama' => $request->nama, @@ -60,7 +69,7 @@ public function store(Request $request) ]); // simpan user login - User::create([ + $user = User::create([ 'name' => $request->nama, 'email' => $request->email, 'password' => Hash::make('12345678'), // 🔥 default @@ -69,6 +78,9 @@ public function store(Request $request) 'is_default_password' => true // 🔥 WAJIB ]); +/** @var \App\Models\User $user */ +app(OtpService::class)->send($user); + return redirect()->route('admin.siswa.index') ->with('success', 'Siswa + akun login berhasil dibuat'); } @@ -120,11 +132,18 @@ public function destroy(Siswa $siswa) } // ================= NONAKTIF ================= - public function nonaktif($id) + public function nonaktif(Request $request, $id) { + $request->validate([ + 'alasan' => 'required|min:5' + ]); + $siswa = Siswa::findOrFail($id); - $siswa->status = 'nonaktif'; - $siswa->save(); + + $siswa->update([ + 'status' => 'nonaktif', + 'alasan_nonaktif' => $request->alasan + ]); return back()->with('success', 'Siswa dinonaktifkan'); } diff --git a/app/Http/Controllers/auth/OtpController.php b/app/Http/Controllers/auth/OtpController.php new file mode 100644 index 0000000..5d787f1 --- /dev/null +++ b/app/Http/Controllers/auth/OtpController.php @@ -0,0 +1,99 @@ +email)->first(); + + if (!$user) { + return back()->with('error', 'Email tidak ditemukan'); + } + + app(\App\Services\OtpService::class)->send($user); + + return back()->with('success', 'OTP berhasil dikirim'); + } + + // ================= LOGIN ================= + public function loginOtp(Request $request) + { + $request->validate([ + 'email' => 'required|email', + 'password' => 'required', + 'otp' => 'nullable|digits:6' + ]); + + $user = \App\Models\User::where('email', $request->email)->first(); + + if (!$user) { + return back()->with('error', 'User tidak ditemukan'); + } + + // 🔐 CEK PASSWORD + if (!\Hash::check($request->password, $user->password)) { + return back()->with('error', 'Password salah'); + } + + // ========================= + // 🔥 KHUSUS 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'); + } + } + + // ========================= + // ✅ LOGIN + // ========================= + \Auth::login($user); + + // 🔥 HAPUS OTP SETELAH DIPAKAI (hanya untuk guru & siswa) + if (in_array($user->role, ['guru', 'siswa'])) { + $user->update([ + 'otp' => null, + 'otp_expired_at' => null + ]); + } + + // ========================= + // 🔁 REDIRECT ROLE + // ========================= + if ($user->role == 'admin') { + return redirect()->route('admin.dashboard'); + } + + if ($user->role == 'guru') { + return redirect()->route('guru.dashboard'); + } + + if ($user->role == 'siswa') { + return redirect()->route('siswa.dashboard'); + } + + return redirect('/'); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/guru/AbsensiController.php b/app/Http/Controllers/guru/AbsensiController.php index 4347ce8..e97e451 100644 --- a/app/Http/Controllers/guru/AbsensiController.php +++ b/app/Http/Controllers/guru/AbsensiController.php @@ -1,14 +1,130 @@ user()->guru->mapel_id); + // 🔥 VALIDASI DI SINI + if (!$mapel) { + return back()->with('error', 'Mapel guru belum diatur!'); } + $siswa = null; + $kelasAktif = null; + $riwayat = null; + + if ($request->kelas_id && $request->mapel_id) { + + $kelasAktif = Kelas::findOrFail($request->kelas_id); + + $siswa = Siswa::where('kelas_id', $request->kelas_id)->get(); + + // 🔥 ambil absensi hari ini + $absensi = Absensi::where('kelas_id', $request->kelas_id) + ->where('mapel_id', $request->mapel_id) + ->whereDate('tanggal', now()) + ->first(); + + if ($absensi) { + $riwayat = AbsensiDetail::with('siswa') + ->where('absensi_id', $absensi->id) + ->get(); + } + } + + return view('guru.absensi.index', compact( + 'kelas', + 'mapel', + 'siswa', + 'kelasAktif', + 'riwayat' + )); +} + + public function buka(Request $request) +{ + $request->validate([ + 'kelas_id' => 'required', + 'mapel_id' => 'required' + ]); + + $absensi = Absensi::create([ + 'guru_id' => auth()->user()->guru->id, + 'kelas_id' => $request->kelas_id, + 'mapel_id' => $request->mapel_id, + 'tanggal' => now(), + ]); + + return redirect()->route('guru.absensi.kelola', $absensi->id); +} + + public function kelola($id) + { + $absensi = Absensi::findOrFail($id); + + $siswa = Siswa::where('kelas_id', $absensi->kelas_id)->get(); + + return view('guru.absensi.kelola', compact('absensi', 'siswa')); + } + + public function simpan(Request $request) +{ + $request->validate([ + 'kelas_id' => 'required', + 'mapel_id' => 'required', + 'absensi' => 'required|array' + ]); + + // 🔥 CEK / BUAT absensi hari ini + $absensi = Absensi::firstOrCreate([ + 'kelas_id' => $request->kelas_id, + 'mapel_id' => $request->mapel_id, + 'tanggal' => now()->toDateString(), + ], [ + 'guru_id' => auth()->user()->guru->id + ]); + + // 🔥 SIMPAN DETAIL + foreach ($request->absensi as $siswaId => $data) { + + AbsensiDetail::updateOrCreate( + [ + 'absensi_id' => $absensi->id, + 'siswa_id' => $siswaId + ], + [ + 'status' => $data['status'], + 'keterangan' => $data['keterangan'] ?? null + ] + ); + } + + return redirect()->back()->with('success', 'Absensi berhasil disimpan'); +} + + public function tutup($id) + { + Absensi::findOrFail($id)->update(['dibuka' => false]); + + return back()->with('success', 'Absensi ditutup'); + } + + public function delete($id) +{ + AbsensiDetail::findOrFail($id)->delete(); + + return back()->with('success', 'Data dihapus'); +} } \ No newline at end of file diff --git a/app/Http/Controllers/guru/MapelController.php b/app/Http/Controllers/guru/MapelController.php new file mode 100644 index 0000000..83550d9 --- /dev/null +++ b/app/Http/Controllers/guru/MapelController.php @@ -0,0 +1,28 @@ +join('mata_pelajarans', 'jadwals.mata_pelajaran_id', '=', 'mata_pelajarans.id') + ->select('mata_pelajarans.id', 'mata_pelajarans.nama_mapel') + ->distinct() + ->get(); + + return response()->json($mapel); + + } catch (\Exception $e) { + return response()->json([ + 'error' => $e->getMessage() + ], 500); + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/guru/TugasController.php b/app/Http/Controllers/guru/TugasController.php new file mode 100644 index 0000000..fddfbe3 --- /dev/null +++ b/app/Http/Controllers/guru/TugasController.php @@ -0,0 +1,175 @@ +user()->guru; + + $tugas = Tugas::with(['kelas', 'mapel']) + ->withCount('pengumpulan') // 🔥 hitung jumlah yang kumpul + ->where('guru_id', $guru->id) + ->get(); + + // 🔥 tambahkan total siswa per kelas + foreach ($tugas as $t) { + $t->total_siswa = Siswa::where('kelas_id', $t->kelas_id)->count(); + } + + return view('guru.tugas.index', compact('tugas')); +} + +public function create() +{ + $kelas = Kelas::all(); + $guru = auth()->user()->guru; + + $mapel = $guru->mapelRel; + + return view('guru.tugas.create', compact('kelas', 'mapel')); +} + + public function store(Request $request) +{ + $guru = auth()->user()->guru; + + $request->validate([ + 'kelas_id' => 'required', + 'judul' => 'required', + 'deadline' => 'required', + 'file' => 'nullable|mimes:pdf,doc,docx,ppt,pptx|max:2048' + ]); + + $filePath = null; + + // 🔥 INI BAGIAN PALING PENTING + if ($request->hasFile('file')) { + $filePath = $request->file('file')->store('tugas_file', 'public'); + } + + Tugas::create([ + 'guru_id' => $guru->id, + 'kelas_id' => $request->kelas_id, + 'mapel_id' => $guru->mapel_id, + 'judul' => $request->judul, + 'deskripsi' => $request->deskripsi, + 'file' => $filePath, + 'deadline' => str_replace('T', ' ', $request->deadline), +]); + + return redirect()->route('guru.tugas.index') + ->with('success', 'Tugas berhasil dibuat'); +} + public function getMapelByKelas($id) + { + $guruId = auth()->user()->guru->id; + + $mapel = Jadwal::where('kelas_id', $id) + ->where('guru_id', $guruId) + ->join('mata_pelajarans', 'jadwals.mata_pelajaran_id', '=', 'mata_pelajarans.id') + ->select('mata_pelajarans.id', 'mata_pelajarans.nama_mapel') + ->distinct() + ->get(); + + return response()->json($mapel); + } + public function show($id) +{ + $tugas = Tugas::with(['kelas', 'mapel'])->findOrFail($id); + + return view('guru.tugas.show', compact('tugas')); +} +public function edit($id) +{ + $tugas = Tugas::findOrFail($id); + $kelas = Kelas::all(); + + return view('guru.tugas.edit', compact('tugas', 'kelas')); +} +public function update(Request $request, $id) +{ + $request->validate([ + 'judul' => 'required', + 'kelas_id' => 'required', + 'deadline' => 'required' + ]); + + $tugas = Tugas::findOrFail($id); + + $tugas->update([ + 'judul' => $request->judul, + 'kelas_id' => $request->kelas_id, + 'deskripsi' => $request->deskripsi, + 'deadline' => $request->deadline, + ]); + + return redirect()->route('guru.tugas.index') + ->with('success', 'Tugas berhasil diupdate'); +} + +public function download($id) +{ + $tugas = Tugas::findOrFail($id); + + if (!$tugas->file) { + abort(404, 'File tidak ditemukan'); + } + + $path = storage_path('app/public/' . $tugas->file); + + if (!file_exists($path)) { + abort(404, 'File tidak ada di storage'); + } + + return response()->download($path); +} + +public function pengumpulan($id) +{ + $tugas = Tugas::with(['pengumpulan.siswa'])->findOrFail($id); + + $deadline = Carbon::parse($tugas->deadline); + + foreach ($tugas->pengumpulan as $p) { + $waktu = Carbon::parse($p->created_at); + + if ($waktu->gt($deadline)) { + $p->status = 'telat'; + } else { + $p->status = 'tepat'; + } + } + + return view('guru.tugas.pengumpulan', compact('tugas')); +} + +public function nilai(Request $request, $id) +{ + $request->validate([ + 'nilai' => 'required|integer|min:0|max:100', + 'komentar' => 'nullable' + ]); + + $pengumpulan = \App\Models\PengumpulanTugas::findOrFail($id); + + $pengumpulan->update([ + 'nilai' => $request->nilai, + 'komentar' => $request->komentar + ]); + + return back()->with('success', 'Nilai berhasil disimpan'); +} +} \ No newline at end of file diff --git a/app/Http/Controllers/guru/UjianController.php b/app/Http/Controllers/guru/UjianController.php new file mode 100644 index 0000000..ad818d4 --- /dev/null +++ b/app/Http/Controllers/guru/UjianController.php @@ -0,0 +1,97 @@ +id())->latest()->get(); + return view('guru.ujian.index', compact('data')); + } + + public function create() + { + $user = Auth::user(); + + // safety check + if (!$user) { + abort(403, 'Belum login'); + } + + $mapel = $user->mapel; // ✔ relasi sudah benar + + return view('guru.ujian.create', compact('mapel')); + } + + public function store(Request $request) + { + $request->validate([ + 'judul' => 'required', + 'soal' => 'required|array' + ]); + + // 🔥 SIMPAN UJIAN + $ujian = Ujian::create([ + 'judul' => $request->judul, + 'durasi' => $request->durasi, + 'mulai' => $request->mulai, + 'selesai' => $request->selesai, + 'guru_id' => auth()->user()->guru_id + ]); + + // 🔥 SIMPAN SOAL + foreach ($request->soal as $s) { + Soal::create([ + 'ujian_id' => $ujian->id, + 'pertanyaan' => $s['pertanyaan'], + 'a' => $s['a'], + 'b' => $s['b'], + 'c' => $s['c'], + 'd' => $s['d'], + 'jawaban_benar' => $s['jawaban'], + ]); + } + + return redirect()->route('guru.ujian.index') + ->with('success', 'Ujian + soal berhasil dibuat'); + } + + public function soal($id) + { + $ujian = Ujian::findOrFail($id); + return view('guru.ujian.soal', compact('ujian')); + } + + public function storeSoal(Request $request, $id) + { + $request->validate([ + 'soal' => 'required|array', + 'soal.*.pertanyaan' => 'required|string', + 'soal.*.a' => 'required|string', + 'soal.*.b' => 'required|string', + 'soal.*.c' => 'required|string', + 'soal.*.d' => 'required|string', + 'soal.*.jawaban' => 'required|in:a,b,c,d', + ]); + + foreach ($request->soal as $s) { + Soal::create([ + 'ujian_id' => $id, + 'pertanyaan' => $s['pertanyaan'], + 'a' => $s['a'], + 'b' => $s['b'], + 'c' => $s['c'], + 'd' => $s['d'], + 'jawaban_benar' => $s['jawaban'], + ]); + } + + return back()->with('success','Soal disimpan'); + } +} diff --git a/app/Http/Controllers/siswa/AbsensiController.php b/app/Http/Controllers/siswa/AbsensiController.php index 60078be..83d6115 100644 --- a/app/Http/Controllers/siswa/AbsensiController.php +++ b/app/Http/Controllers/siswa/AbsensiController.php @@ -1,17 +1,82 @@ user()->siswa; + + $absensis = Absensi::with(['mapel', 'guru']) + ->where('kelas_id', $siswa->kelas_id) + ->where('dibuka', true) + ->where(function ($q) { + $q->whereNull('waktu_selesai') + ->orWhere('waktu_selesai', '>', now()); + }) + ->latest() + ->get(); + + // 🔥 GROUP BY MAPEL (INI YANG KURANG) + $groupedAbsensi = $absensis->groupBy(function ($item) { + return $item->mapel->nama_mapel ?? 'Tanpa Mapel'; + }); + + // 🔥 CEK SUDAH ABSEN + $sudahAbsen = AbsensiDetail::where('siswa_id', $siswa->id) + ->pluck('absensi_id') + ->toArray(); + + return view('siswa.absensi.index', compact('groupedAbsensi', 'sudahAbsen')); +} + + // 🔹 SCAN QR + public function scan($token) { - $user = Auth::user(); + $absensi = Absensi::where('token', $token)->firstOrFail(); - $absensi = Absensi::where('siswa_id', $user->siswa_id)->get(); - - return view('siswa.absensi.index', compact('absensi')); + return view('siswa.absensi.scan', compact('absensi')); } + + // 🔹 SUBMIT HADIR + public function hadir(Request $request) +{ + $request->validate([ + 'absensi_id' => 'required|exists:absensi,id' + ]); + + $absensi = Absensi::findOrFail($request->absensi_id); + + // 🔥 VALIDASI WAKTU + if ($absensi->waktu_selesai && now()->greaterThan($absensi->waktu_selesai)) { + return back()->with('error', 'Waktu absensi sudah habis!'); + } + + $siswa = auth()->user()->siswa; + + $cek = AbsensiDetail::where('absensi_id', $absensi->id) + ->where('siswa_id', $siswa->id) + ->exists(); + + if ($cek) { + return back()->with('error', 'Kamu sudah absen!'); + } + + AbsensiDetail::create([ + 'absensi_id' => $absensi->id, + 'siswa_id' => $siswa->id, + 'status' => 'hadir', + 'waktu_absen' => now() + ]); + + return back()->with('success', 'Absensi berhasil!'); +} } \ No newline at end of file diff --git a/app/Http/Controllers/siswa/TugasController.php b/app/Http/Controllers/siswa/TugasController.php new file mode 100644 index 0000000..618a392 --- /dev/null +++ b/app/Http/Controllers/siswa/TugasController.php @@ -0,0 +1,91 @@ +user()->siswa; + + $tugas = Tugas::with('mapel') + ->where('kelas_id', $siswa->kelas_id) + ->get(); + + foreach ($tugas as $t) { + + $pengumpulan = \App\Models\PengumpulanTugas::where('tugas_id', $t->id) + ->where('siswa_id', $siswa->id) + ->first(); + + $deadline = \Carbon\Carbon::parse($t->deadline); + + $t->waktu_kumpul = null; + $t->nilai = null; + $t->komentar = null; + $t->status = 'belum'; + + if ($pengumpulan) { + $t->waktu_kumpul = $pengumpulan->created_at; + $t->nilai = $pengumpulan->nilai; + $t->komentar = $pengumpulan->komentar; + + if ($pengumpulan->created_at > $deadline) { + $t->status = 'telat'; + } else { + $t->status = 'tepat'; + } + } else { + if (now()->gt($deadline)) { + $t->status = 'lewat'; + } + } + } + + return view('siswa.tugas.index', compact('tugas')); +} + + public function kumpul($id) +{ + $tugas = Tugas::findOrFail($id); + + if (now()->gt($tugas->deadline)) { + return redirect()->route('siswa.tugas.index') + ->with('error', 'Deadline sudah lewat, tidak bisa mengumpulkan tugas'); + } + + return view('siswa.tugas.kumpul', compact('tugas')); +} + + public function store(Request $request, $id) +{ + $tugas = Tugas::findOrFail($id); + + // 🔥 BLOCK UTAMA + if (now()->gt($tugas->deadline)) { + return redirect()->route('siswa.tugas.index') + ->with('error', 'Tugas sudah melewati deadline'); + } + + $request->validate([ + 'file' => 'required|mimes:pdf,doc,docx|max:2048' + ]); + + $filePath = $request->file('file')->store('jawaban_tugas', 'public'); + + PengumpulanTugas::create([ + 'tugas_id' => $id, + 'siswa_id' => auth()->user()->siswa->id, + 'file' => $filePath + ]); + + return redirect()->route('siswa.tugas.index') + ->with('success', 'Tugas berhasil dikumpulkan'); +} +} \ No newline at end of file diff --git a/app/Http/Controllers/siswa/UjianController.php b/app/Http/Controllers/siswa/UjianController.php new file mode 100644 index 0000000..6c27097 --- /dev/null +++ b/app/Http/Controllers/siswa/UjianController.php @@ -0,0 +1,52 @@ +get(); + + return view('siswa.ujian.kerjakan', compact('ujian','soals')); + } + public function submit(Request $request, $id) +{ + $soals = Soal::where('ujian_id', $id)->get(); + + $benar = 0; + $total = $soals->count(); + + foreach ($soals as $s) { + $jawabanUser = $request->jawaban[$s->id] ?? null; + + if ($jawabanUser == $s->jawaban_benar) { + $benar++; + } + } + + $nilai = ($benar / $total) * 100; + + return view('siswa.ujian.hasil', [ + 'nilai' => $nilai, + 'benar' => $benar, + 'total' => $total + ]); +} + + +} diff --git a/app/Models/Absensi.php b/app/Models/Absensi.php new file mode 100644 index 0000000..6d6a3d0 --- /dev/null +++ b/app/Models/Absensi.php @@ -0,0 +1,45 @@ +hasMany(AbsensiDetail::class); + } + + public function guru() + { + return $this->belongsTo(Guru::class); + } + + public function kelas() + { + return $this->belongsTo(Kelas::class); + } + + public function mapel() + { + return $this->belongsTo(MataPelajaran::class, 'mapel_id'); + } +} \ No newline at end of file diff --git a/app/Models/AbsensiDetail.php b/app/Models/AbsensiDetail.php new file mode 100644 index 0000000..c2e71fa --- /dev/null +++ b/app/Models/AbsensiDetail.php @@ -0,0 +1,30 @@ +belongsTo(Absensi::class); + } + + public function siswa() + { + return $this->belongsTo(Siswa::class); + } +} \ No newline at end of file diff --git a/app/Models/Guru.php b/app/Models/Guru.php index d50ebbf..6abddd4 100644 --- a/app/Models/Guru.php +++ b/app/Models/Guru.php @@ -10,7 +10,7 @@ class Guru extends Model use HasFactory; protected $table = 'gurus'; // nama tabel - protected $fillable = ['nama', 'nip', 'mapel','alamat','telepon','status']; + protected $fillable = ['nama', 'nip','alamat','telepon','status', 'mapel_id']; public function user() { return $this->hasOne(User::class, 'guru_id'); @@ -20,5 +20,15 @@ public function jadwal() { return $this->hasMany(\App\Models\Jadwal::class); } + +public function mapel() +{ + return $this->belongsTo(MataPelajaran::class, 'mapel_id'); +} + +public function mapelRel() +{ + return $this->belongsTo(MataPelajaran::class, 'mapel_id'); +} } diff --git a/app/Models/Hasil.php b/app/Models/Hasil.php new file mode 100644 index 0000000..bbdcb10 --- /dev/null +++ b/app/Models/Hasil.php @@ -0,0 +1,16 @@ +belongsTo(Ujian::class); + } +} diff --git a/app/Models/Jadwal.php b/app/Models/Jadwal.php index f44f875..046febd 100644 --- a/app/Models/Jadwal.php +++ b/app/Models/Jadwal.php @@ -2,7 +2,7 @@ namespace App\Models; -use Illuminate\Database\Eloquent\Model; // 🔥 INI WAJIB! +use Illuminate\Database\Eloquent\Model; class Jadwal extends Model { @@ -25,8 +25,8 @@ public function mapel() return $this->belongsTo(MataPelajaran::class, 'mata_pelajaran_id'); } - public function guru() - { - return $this->belongsTo(Guru::class); - } + public function guru() +{ + return $this->belongsTo(Guru::class, 'guru_id'); +} } \ No newline at end of file diff --git a/app/Models/Jawaban.php b/app/Models/Jawaban.php new file mode 100644 index 0000000..1816732 --- /dev/null +++ b/app/Models/Jawaban.php @@ -0,0 +1,16 @@ +belongsTo(Soal::class); + } +} diff --git a/app/Models/MataPelajaran.php b/app/Models/MataPelajaran.php index 0b82147..7899038 100644 --- a/app/Models/MataPelajaran.php +++ b/app/Models/MataPelajaran.php @@ -13,4 +13,9 @@ class MataPelajaran extends Model 'kode_mapel', 'jam_pelajaran' ]; + + public function jadwal() +{ + return $this->hasMany(Jadwal::class, 'mata_pelajaran_id'); +} } \ No newline at end of file diff --git a/app/Models/PengumpulanTugas.php b/app/Models/PengumpulanTugas.php new file mode 100644 index 0000000..e2550a3 --- /dev/null +++ b/app/Models/PengumpulanTugas.php @@ -0,0 +1,31 @@ +belongsTo(Siswa::class); +} + +public function tugas() +{ + return $this->belongsTo(Tugas::class); +} +} \ No newline at end of file diff --git a/app/Models/Siswa.php b/app/Models/Siswa.php index 137cbe3..726ef27 100644 --- a/app/Models/Siswa.php +++ b/app/Models/Siswa.php @@ -16,7 +16,8 @@ class Siswa extends Model 'kelas_id', 'alamat', 'telepon', - 'status' + 'status', + 'alasan_nonaktif' ]; public $timestamps = true; diff --git a/app/Models/Soal.php b/app/Models/Soal.php new file mode 100644 index 0000000..e192277 --- /dev/null +++ b/app/Models/Soal.php @@ -0,0 +1,18 @@ +belongsTo(Ujian::class); + } +} diff --git a/app/Models/Tugas.php b/app/Models/Tugas.php new file mode 100644 index 0000000..1d9074a --- /dev/null +++ b/app/Models/Tugas.php @@ -0,0 +1,43 @@ +belongsTo(Kelas::class, 'kelas_id'); // 🔥 WAJIB ADA INI +} + +public function mapel() +{ + return $this->belongsTo(MataPelajaran::class, 'mapel_id'); +} + +public function guru() +{ + return $this->belongsTo(Guru::class, 'guru_id'); +} + + public function pengumpulan() +{ + return $this->hasMany(\App\Models\PengumpulanTugas::class, 'tugas_id'); +} +} \ No newline at end of file diff --git a/app/Models/Ujian.php b/app/Models/Ujian.php new file mode 100644 index 0000000..8ee40eb --- /dev/null +++ b/app/Models/Ujian.php @@ -0,0 +1,20 @@ +hasMany(Soal::class); + } + + public function guru() + { + return $this->belongsTo(User::class, 'guru_id'); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 068a3fc..4e20a12 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -27,6 +27,8 @@ class User extends Authenticatable 'siswa_id', 'mapel_id', 'photo', + 'otp', + 'otp_expired_at', 'telepon', // 🔥 WAJIB TAMBAH INI 'is_default_password' ]; diff --git a/app/Services/OtpService.php b/app/Services/OtpService.php new file mode 100644 index 0000000..d31dd63 --- /dev/null +++ b/app/Services/OtpService.php @@ -0,0 +1,72 @@ +update([ + 'otp' => $otp, + 'otp_expired_at' => now()->addMinutes(5) + ]); + + // ================= EMAIL ================= + \Mail::raw("Kode OTP kamu: $otp", function ($msg) use ($user) { + $msg->to($user->email) + ->subject('Kode OTP Login'); + }); + + // ================= AMBIL TELEPON ================= + $telepon = null; + + if ($user->siswa && $user->siswa->telepon) { + $telepon = $user->siswa->telepon; + } elseif ($user->guru && $user->guru->telepon) { + $telepon = $user->guru->telepon; + } elseif ($user->telepon) { + $telepon = $user->telepon; + } + + // ================= KIRIM WA ================= + if ($telepon) { + $this->sendWA($telepon, $otp); + } else { + \Log::error('NO HP TIDAK ADA UNTUK USER', [ + 'user_id' => $user->id, + 'role' => $user->role + ]); + } +} + + private function sendWA($no, $otp) + { + $no = preg_replace('/[^0-9]/', '', $no); + + if (substr($no, 0, 1) === '0') { + $no = '62' . substr($no, 1); + } + + $token = env('FONNTE_TOKEN'); + + $response = Http::asForm()->withHeaders([ + 'Authorization' => $token + ])->post('https://api.fonnte.com/send', [ + 'target' => $no, + 'message' => "Kode OTP kamu: $otp" + ]); + + Log::info('WA RESULT', [ + 'nomor' => $no, + 'response' => $response->json() + ]); + } +} \ No newline at end of file diff --git a/composer.json b/composer.json index 441e68f..cedc908 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,8 @@ "require": { "php": "^8.2", "laravel/framework": "^11.31", - "laravel/tinker": "^2.9" + "laravel/tinker": "^2.9", + "simplesoftwareio/simple-qrcode": "^4.2" }, "require-dev": { "fakerphp/faker": "^1.23", diff --git a/composer.lock b/composer.lock index 8716703..03f90a7 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,68 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d6c083b04e73afd8ea1cc978bddab18d", + "content-hash": "ba0b871c4e08ecaa364989ce5e6f5138", "packages": [ + { + "name": "bacon/bacon-qr-code", + "version": "2.0.8", + "source": { + "type": "git", + "url": "https://github.com/Bacon/BaconQrCode.git", + "reference": "8674e51bb65af933a5ffaf1c308a660387c35c22" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/8674e51bb65af933a5ffaf1c308a660387c35c22", + "reference": "8674e51bb65af933a5ffaf1c308a660387c35c22", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "dasprid/enum": "^1.0.3", + "ext-iconv": "*", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phly/keep-a-changelog": "^2.1", + "phpunit/phpunit": "^7 | ^8 | ^9", + "spatie/phpunit-snapshot-assertions": "^4.2.9", + "squizlabs/php_codesniffer": "^3.4" + }, + "suggest": { + "ext-imagick": "to generate QR code images" + }, + "type": "library", + "autoload": { + "psr-4": { + "BaconQrCode\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Ben Scholzen 'DASPRiD'", + "email": "mail@dasprids.de", + "homepage": "https://dasprids.de/", + "role": "Developer" + } + ], + "description": "BaconQrCode is a QR code generator for PHP.", + "homepage": "https://github.com/Bacon/BaconQrCode", + "support": { + "issues": "https://github.com/Bacon/BaconQrCode/issues", + "source": "https://github.com/Bacon/BaconQrCode/tree/2.0.8" + }, + "time": "2022-12-07T17:46:57+00:00" + }, { "name": "brick/math", "version": "0.12.3", @@ -135,6 +195,62 @@ ], "time": "2024-02-09T16:56:22+00:00" }, + { + "name": "dasprid/enum", + "version": "1.0.7", + "source": { + "type": "git", + "url": "https://github.com/DASPRiD/Enum.git", + "reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/b5874fa9ed0043116c72162ec7f4fb50e02e7cce", + "reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.1 <9.0" + }, + "require-dev": { + "phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11", + "squizlabs/php_codesniffer": "*" + }, + "type": "library", + "autoload": { + "psr-4": { + "DASPRiD\\Enum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Ben Scholzen 'DASPRiD'", + "email": "mail@dasprids.de", + "homepage": "https://dasprids.de/", + "role": "Developer" + } + ], + "description": "PHP 7.1 enum implementation", + "keywords": [ + "enum", + "map" + ], + "support": { + "issues": "https://github.com/DASPRiD/Enum/issues", + "source": "https://github.com/DASPRiD/Enum/tree/1.0.7" + }, + "time": "2025-09-16T12:23:56+00:00" + }, { "name": "dflydev/dot-access-data", "version": "v3.0.3", @@ -3272,6 +3388,80 @@ }, "time": "2025-06-25T14:20:11+00:00" }, + { + "name": "simplesoftwareio/simple-qrcode", + "version": "4.2.0", + "source": { + "type": "git", + "url": "https://github.com/SimpleSoftwareIO/simple-qrcode.git", + "reference": "916db7948ca6772d54bb617259c768c9cdc8d537" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/SimpleSoftwareIO/simple-qrcode/zipball/916db7948ca6772d54bb617259c768c9cdc8d537", + "reference": "916db7948ca6772d54bb617259c768c9cdc8d537", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "bacon/bacon-qr-code": "^2.0", + "ext-gd": "*", + "php": ">=7.2|^8.0" + }, + "require-dev": { + "mockery/mockery": "~1", + "phpunit/phpunit": "~9" + }, + "suggest": { + "ext-imagick": "Allows the generation of PNG QrCodes.", + "illuminate/support": "Allows for use within Laravel." + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "QrCode": "SimpleSoftwareIO\\QrCode\\Facades\\QrCode" + }, + "providers": [ + "SimpleSoftwareIO\\QrCode\\QrCodeServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "SimpleSoftwareIO\\QrCode\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Simple Software LLC", + "email": "support@simplesoftware.io" + } + ], + "description": "Simple QrCode is a QR code generator made for Laravel.", + "homepage": "https://www.simplesoftware.io/#/docs/simple-qrcode", + "keywords": [ + "Simple", + "generator", + "laravel", + "qrcode", + "wrapper" + ], + "support": { + "issues": "https://github.com/SimpleSoftwareIO/simple-qrcode/issues", + "source": "https://github.com/SimpleSoftwareIO/simple-qrcode/tree/4.2.0" + }, + "time": "2021-02-08T20:43:55+00:00" + }, { "name": "symfony/clock", "version": "v7.3.0", diff --git a/config/app.php b/config/app.php index f467267..f092435 100644 --- a/config/app.php +++ b/config/app.php @@ -65,7 +65,7 @@ | */ - 'timezone' => env('APP_TIMEZONE', 'UTC'), + 'timezone' => 'Asia/Jakarta', /* |-------------------------------------------------------------------------- diff --git a/database/factories/HasilFactory.php b/database/factories/HasilFactory.php new file mode 100644 index 0000000..64ea655 --- /dev/null +++ b/database/factories/HasilFactory.php @@ -0,0 +1,23 @@ + + */ +class HasilFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + // + ]; + } +} diff --git a/database/factories/JawabanFactory.php b/database/factories/JawabanFactory.php new file mode 100644 index 0000000..f86adae --- /dev/null +++ b/database/factories/JawabanFactory.php @@ -0,0 +1,23 @@ + + */ +class JawabanFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + // + ]; + } +} diff --git a/database/factories/SoalFactory.php b/database/factories/SoalFactory.php new file mode 100644 index 0000000..9b2b2fd --- /dev/null +++ b/database/factories/SoalFactory.php @@ -0,0 +1,23 @@ + + */ +class SoalFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + // + ]; + } +} diff --git a/database/factories/UjianFactory.php b/database/factories/UjianFactory.php new file mode 100644 index 0000000..5fd99c8 --- /dev/null +++ b/database/factories/UjianFactory.php @@ -0,0 +1,23 @@ + + */ +class UjianFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + // + ]; + } +} diff --git a/database/migrations/2026_04_17_084158_create_tugas_table.php b/database/migrations/2026_04_17_084158_create_tugas_table.php new file mode 100644 index 0000000..87b26ab --- /dev/null +++ b/database/migrations/2026_04_17_084158_create_tugas_table.php @@ -0,0 +1,33 @@ +id(); + $table->foreignId('guru_id')->constrained()->cascadeOnDelete(); + $table->foreignId('kelas_id')->constrained()->cascadeOnDelete(); + $table->foreignId('mapel_id')->constrained()->cascadeOnDelete(); + $table->string('judul'); + $table->text('deskripsi'); + $table->date('deadline'); + $table->timestamps(); +}); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('tugas'); + } +}; diff --git a/database/migrations/2026_04_17_084159_create_pengumpulan_tugas_table.php b/database/migrations/2026_04_17_084159_create_pengumpulan_tugas_table.php new file mode 100644 index 0000000..2c37b04 --- /dev/null +++ b/database/migrations/2026_04_17_084159_create_pengumpulan_tugas_table.php @@ -0,0 +1,31 @@ +id(); + $table->foreignId('tugas_id')->constrained()->cascadeOnDelete(); + $table->foreign('siswa_id')->references('id')->on('siswa')->onDelete('cascade'); + $table->string('file')->nullable(); + $table->integer('nilai')->nullable(); + $table->timestamps(); +}); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('pengumpulan_tugas'); + } +}; diff --git a/database/migrations/2026_04_17_105854_add_mapel_to_gurus_table.php b/database/migrations/2026_04_17_105854_add_mapel_to_gurus_table.php new file mode 100644 index 0000000..ccfebe0 --- /dev/null +++ b/database/migrations/2026_04_17_105854_add_mapel_to_gurus_table.php @@ -0,0 +1,25 @@ +string('mapel')->nullable(); + }); +} + +public function down() +{ + Schema::table('gurus', function (Blueprint $table) { + $table->dropColumn('mapel'); + }); +} +}; diff --git a/database/migrations/2026_04_20_024223_add_file_to_tugas_table.php b/database/migrations/2026_04_20_024223_add_file_to_tugas_table.php new file mode 100644 index 0000000..92653b3 --- /dev/null +++ b/database/migrations/2026_04_20_024223_add_file_to_tugas_table.php @@ -0,0 +1,22 @@ +string('file')->nullable(); + }); + } + + public function down() + { + Schema::table('tugas', function (Blueprint $table) { + $table->dropColumn('file'); + }); + } +}; diff --git a/database/migrations/2026_04_20_044829_update_deadline_to_datetime_in_tugas.php b/database/migrations/2026_04_20_044829_update_deadline_to_datetime_in_tugas.php new file mode 100644 index 0000000..d106b13 --- /dev/null +++ b/database/migrations/2026_04_20_044829_update_deadline_to_datetime_in_tugas.php @@ -0,0 +1,25 @@ +dateTime('deadline')->change(); + }); +} + +public function down() +{ + Schema::table('tugas', function (Blueprint $table) { + $table->date('deadline')->change(); + }); +} +}; diff --git a/database/migrations/2026_04_20_115943_add_komentar_to_pengumpulan_tugas.php b/database/migrations/2026_04_20_115943_add_komentar_to_pengumpulan_tugas.php new file mode 100644 index 0000000..9aaf530 --- /dev/null +++ b/database/migrations/2026_04_20_115943_add_komentar_to_pengumpulan_tugas.php @@ -0,0 +1,32 @@ +text('komentar')->nullable(); + } + + }); +} + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('pengumpulan_tugas', function (Blueprint $table) { + // + }); + } +}; diff --git a/database/migrations/2026_04_20_123724_create_absensi_table.php b/database/migrations/2026_04_20_123724_create_absensi_table.php new file mode 100644 index 0000000..4dc40bd --- /dev/null +++ b/database/migrations/2026_04_20_123724_create_absensi_table.php @@ -0,0 +1,27 @@ +id(); + + $table->foreignId('guru_id')->constrained()->cascadeOnDelete(); + $table->foreignId('kelas_id')->constrained()->cascadeOnDelete(); + $table->foreignId('mapel_id')->constrained()->cascadeOnDelete(); + + $table->date('tanggal'); + + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('absensi'); + } +}; diff --git a/database/migrations/2026_04_20_123747_create_absensi_detail_table.php b/database/migrations/2026_04_20_123747_create_absensi_detail_table.php new file mode 100644 index 0000000..c8d453b --- /dev/null +++ b/database/migrations/2026_04_20_123747_create_absensi_detail_table.php @@ -0,0 +1,28 @@ +id(); + + $table->foreignId('absensi_id')->constrained('absensi')->cascadeOnDelete(); + $table->foreignId('siswa_id')->constrained()->cascadeOnDelete(); + + $table->enum('status', ['hadir', 'izin', 'sakit', 'alpha'])->default('hadir'); + + $table->text('keterangan')->nullable(); // 🔥 WAJIB + + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('absensi_detail'); + } +}; diff --git a/database/migrations/2026_04_28_151150_add_alasan_nonaktif_to_siswa_table.php b/database/migrations/2026_04_28_151150_add_alasan_nonaktif_to_siswa_table.php new file mode 100644 index 0000000..a113f96 --- /dev/null +++ b/database/migrations/2026_04_28_151150_add_alasan_nonaktif_to_siswa_table.php @@ -0,0 +1,28 @@ +text('alasan_nonaktif')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('siswa', function (Blueprint $table) { + // + }); + } +}; diff --git a/database/migrations/2026_04_30_142041_create_jawabans_table.php b/database/migrations/2026_04_30_142041_create_jawabans_table.php new file mode 100644 index 0000000..9bb93d2 --- /dev/null +++ b/database/migrations/2026_04_30_142041_create_jawabans_table.php @@ -0,0 +1,27 @@ +id(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('jawabans'); + } +}; diff --git a/database/migrations/2026_04_30_142041_create_ujians_table.php b/database/migrations/2026_04_30_142041_create_ujians_table.php new file mode 100644 index 0000000..da474a2 --- /dev/null +++ b/database/migrations/2026_04_30_142041_create_ujians_table.php @@ -0,0 +1,27 @@ +id(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('ujians'); + } +}; diff --git a/database/migrations/2026_04_30_142137_create_hasils_table.php b/database/migrations/2026_04_30_142137_create_hasils_table.php new file mode 100644 index 0000000..d4b0f03 --- /dev/null +++ b/database/migrations/2026_04_30_142137_create_hasils_table.php @@ -0,0 +1,23 @@ +id(); + $table->foreignId('user_id')->constrained('users')->cascadeOnDelete(); + $table->foreignId('ujian_id')->constrained('ujians')->cascadeOnDelete(); + $table->unsignedInteger('nilai'); + $table->timestamps(); + + $table->unique(['user_id','ujian_id']); // 1 hasil per ujian + }); + } + public function down(): void { + Schema::dropIfExists('hasils'); + } +}; diff --git a/database/migrations/2026_04_30_142137_create_jawabans_table.php b/database/migrations/2026_04_30_142137_create_jawabans_table.php new file mode 100644 index 0000000..c9fd3ae --- /dev/null +++ b/database/migrations/2026_04_30_142137_create_jawabans_table.php @@ -0,0 +1,23 @@ +id(); + $table->foreignId('user_id')->constrained('users')->cascadeOnDelete(); + $table->foreignId('soal_id')->constrained('soals')->cascadeOnDelete(); + $table->enum('jawaban', ['a','b','c','d'])->nullable(); + $table->timestamps(); + + $table->unique(['user_id','soal_id']); // 1 siswa 1 jawaban/soal + }); + } + public function down(): void { + Schema::dropIfExists('jawabans'); + } +}; diff --git a/database/migrations/2026_04_30_142137_create_soals_table.php b/database/migrations/2026_04_30_142137_create_soals_table.php new file mode 100644 index 0000000..35764ec --- /dev/null +++ b/database/migrations/2026_04_30_142137_create_soals_table.php @@ -0,0 +1,25 @@ +id(); + $table->foreignId('ujian_id')->constrained('ujians')->cascadeOnDelete(); + $table->text('pertanyaan'); + $table->string('a'); + $table->string('b'); + $table->string('c'); + $table->string('d'); + $table->enum('jawaban_benar', ['a','b','c','d']); + $table->timestamps(); + }); + } + public function down(): void { + Schema::dropIfExists('soals'); + } +}; diff --git a/database/migrations/2026_04_30_142137_create_ujians_table.php b/database/migrations/2026_04_30_142137_create_ujians_table.php new file mode 100644 index 0000000..97f9398 --- /dev/null +++ b/database/migrations/2026_04_30_142137_create_ujians_table.php @@ -0,0 +1,23 @@ +id(); + $table->string('judul'); + $table->foreignId('guru_id')->constrained('users')->cascadeOnDelete(); + $table->unsignedInteger('durasi')->nullable(); // menit + $table->timestamp('mulai')->nullable(); + $table->timestamp('selesai')->nullable(); + $table->timestamps(); + }); + } + public function down(): void { + Schema::dropIfExists('ujians'); + } +}; diff --git a/resources/views/admin/guru/create.blade.php b/resources/views/admin/guru/create.blade.php index 0970c7c..97a4537 100644 --- a/resources/views/admin/guru/create.blade.php +++ b/resources/views/admin/guru/create.blade.php @@ -82,7 +82,7 @@ Kembali - diff --git a/resources/views/admin/guru/index.blade.php b/resources/views/admin/guru/index.blade.php index 42efd90..66345a1 100644 --- a/resources/views/admin/guru/index.blade.php +++ b/resources/views/admin/guru/index.blade.php @@ -204,7 +204,7 @@ - {{ $g->mapel }} + {{ $g->mapel->nama_mapel ?? '-' }} diff --git a/resources/views/admin/siswa/create.blade.php b/resources/views/admin/siswa/create.blade.php index f4c4ddf..79ef754 100644 --- a/resources/views/admin/siswa/create.blade.php +++ b/resources/views/admin/siswa/create.blade.php @@ -89,9 +89,10 @@ Kembali - + diff --git a/resources/views/admin/siswa/index.blade.php b/resources/views/admin/siswa/index.blade.php index 690bae6..589b7e0 100644 --- a/resources/views/admin/siswa/index.blade.php +++ b/resources/views/admin/siswa/index.blade.php @@ -251,10 +251,18 @@ class="btn btn-warning btn-sm"> @if($item->status == 'aktif') - - Nonaktif - +
+ @csrf + + + + +
@else diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index 26a0990..166b9d3 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -242,19 +242,33 @@ @endif -
+ {{-- FORM KIRIM OTP --}} + @csrf - - + - - - +
+ + {{-- FORM LOGIN --}} +
+ @csrf + + + + + + +
+
+ + Lupa Password? + +
diff --git a/resources/views/auth/reset-password.blade.php b/resources/views/auth/reset-password.blade.php index cd0aab1..8387a2f 100644 --- a/resources/views/auth/reset-password.blade.php +++ b/resources/views/auth/reset-password.blade.php @@ -15,14 +15,20 @@
{{ session('success') }}
@endif -
+ @csrf - + - +
+ +
- +
+ +
+ +
diff --git a/resources/views/components/sidebar.blade.php b/resources/views/components/sidebar.blade.php index 64d0f89..3541e6e 100644 --- a/resources/views/components/sidebar.blade.php +++ b/resources/views/components/sidebar.blade.php @@ -63,20 +63,7 @@ - - - + @@ -84,13 +71,13 @@ {{-- ================= GURU (NAVBAR MOBILE) ================= --}} -@if($role == 'guru') +@if(auth()->check() && auth()->user()->role == 'guru')