param = $quiz; } public function index(Request $request) { $nip = Auth::user()->guru->nip; $matpel = MataPelajaran::where('guru_nip', $nip)->get(); $kelas = Kelas::all(); $tahunAjaran = TahunAjaran::where('status', 'aktif')->get(); // Get quizzes created by this teacher $quiz = Quizzes::with(['mataPelajaran', 'quizLevelSetting']) ->whereHas('mataPelajaran', function ($q) use ($nip) { $q->where('guru_nip', $nip); }) ->orderBy('created_at', 'desc') ->get(); return view("pages.role_guru.quiz.index", compact(['matpel', 'kelas', 'tahunAjaran', 'quiz'])); } public function getQuizByMatpel($id) { $nip = Auth::user()->guru->nip; $quizzes = Quizzes::where('matapelajaran_id', $id) ->whereHas('mataPelajaran', function ($q) use ($nip) { $q->where('guru_nip', $nip); }) ->get(); return response()->json($quizzes); } public function excelDownload() { return Excel::download(new QuizExport, "format_excel_untuk_quiz.xlsx"); } public function preview(Request $request) { $request->validate([ 'file' => 'required|mimes:xlsx,xls' ]); $data = Excel::toArray([], $request->file('file')); // Ambil sheet pertama $rows = $data[0]; $filteredRows = []; $jumlahSoalPerLevel = []; $totalSoalPeLevel = []; $batasNaikLevel = []; $skorLevel = []; foreach ($rows as $index => $row) { if ($index == 0 || !empty($row[1])) { $filteredRows[] = $row; $level = $row[3]; $skor = $row[8]; if (!empty($level) && $index != 0) { $key = 'level' . $level; if (!isset($jumlahSoalPerLevel[$key])) { $jumlahSoalPerLevel[$key] = 0; $totalSoalPeLevel[$key] = 0; } $jumlahSoalPerLevel[$key]++; $totalSoalPeLevel[$key]++; $skorLevel[$key] = $skor; } } } // Hitung batas naik level = 50% dari jumlah soal di level tersebut foreach ($jumlahSoalPerLevel as $key => $jumlah) { // Ambil level dari key, contoh: 'level2' → 2 $level = str_replace('level', '', $key); $keyFase = 'fase' . $level; // Hitung 50% lalu dibulatkan ke atas (ceil) $batasNaikLevel[$keyFase] = (int) ceil($jumlah * 0.5); $jumlahSoalPerLevel[$key] = (int) ceil($jumlah * 0.5); } $soalCount = count($filteredRows) - 1; // Simpan preview dan jumlah soal ke session Session::put('judul', $request->judul); Session::put('deskripsi', $request->deskripsi); Session::put('matapelajaran_id', $request->matapelajaran_id); Session::put('waktu', $request->waktu); Session::put('preview_soal', $filteredRows); Session::put('total_soal', $soalCount); Session::put('total_soal_tampil', $request->total_soal_tampil ?? 20); Session::put('uploaded_filename', $request->file('file')->getClientOriginalName()); // quiz level settings Session::put('total_soal_per_level', $totalSoalPeLevel); Session::put('jumlah_soal_per_level', $jumlahSoalPerLevel); Session::put('level_awal', $request->level_awal); Session::put('batas_naik_level', $batasNaikLevel); Session::put('skor_level', $skorLevel); Session::put('kkm', $request->kkm); return redirect()->back(); } protected function removeSession() { session()->forget('judul'); session()->forget('deskripsi'); session()->forget('matapelajaran_id'); session()->forget('waktu'); session()->forget('preview_soal'); session()->forget('total_soal'); session()->forget('total_soal_tampil'); session()->forget('uploaded_filename'); // quiz level settings session()->forget('total_soal_per_level'); session()->forget('jumlah_soal_per_level'); session()->forget('level_awal'); session()->forget('batas_naik_level'); session()->forget('skor_level'); session()->forget('kkm'); } public function resetPreview() { $this->removeSession(); return redirect()->back()->with('success', 'Data preview berhasil direset.'); } /** * Show the form for creating a new resource. */ public function create() { $nip = Auth::user()->guru->nip; $matpel = MataPelajaran::where("guru_nip", $nip)->get(); $naik_level = json_encode(session('batas_naik_level') ?? []); return view("pages.role_guru.quiz.create", compact(['matpel', 'naik_level'])); } /** * Store a newly created resource in storage. */ public function store(Request $request) { $preview = session('preview_soal'); if (!is_array($preview)) { return redirect()->back()->with('validation_error', [ 'title' => 'Error', 'message' => 'Data soal tidak ditemukan atau session sudah habis. Silakan ulangi proses import/preview quiz.' ]); } array_shift($preview); if (!$preview || count($preview) <= 1) { \Log::error('Quiz Import Error: Tidak ada data untuk disimpan'); Alert::error("Terjadi Kesalahan", "Tidak ada data untuk disimpan."); return redirect()->back(); } if ($request->total_soal_tampil < 10) { \Log::error('Quiz Import Error: Jumlah soal minimal 10'); Alert::error("Terjadi Kesalahan", "Jumlah soal minimal 10."); return redirect()->back(); } // ======================================== // VALIDASI KETAT UNTUK MENCEGAH BUG QUIZ // ======================================== // 1. VALIDASI: Total soal per level harus sama dengan total soal tampil $totalSoalPerLevel = 0; foreach ($request->jumlah_soal_per_level as $key => $value) { $totalSoalPerLevel += (int) $value; } if ($totalSoalPerLevel != $request->total_soal_tampil) { \Log::error('Quiz Import Error: Total soal per level tidak sama dengan total soal tampil'); \Log::error('Detail: Total soal per level = ' . $totalSoalPerLevel . ', Total soal tampil = ' . $request->total_soal_tampil); \Log::error('Detail per level: ' . json_encode($request->jumlah_soal_per_level)); return redirect()->back()->with('validation_error', [ 'title' => 'Error Validasi', 'message' => "Total soal per level ($totalSoalPerLevel) harus sama dengan total soal tampil ({$request->total_soal_tampil}).

POTENSI BUG: Jika tidak sama, siswa bisa stuck di level tertentu karena soal tidak cukup.
CATATAN: Pastikan jumlah soal per level dijumlahkan sama dengan total soal tampil." ]); } // 2. VALIDASI: Hitung jumlah soal yang tersedia per level dari data import $levelCounts = []; foreach ($preview as $row) { if (!empty($row[3])) { // level $level = $row[3]; $levelCounts[$level] = ($levelCounts[$level] ?? 0) + 1; } } // 3. VALIDASI: Jumlah soal per level tidak boleh melebihi soal yang tersedia foreach ($request->jumlah_soal_per_level as $key => $value) { $level = str_replace('level', '', $key); $availableInLevel = $levelCounts[$level] ?? 0; if ($value > $availableInLevel) { \Log::error('Quiz Import Error: Jumlah soal setting melebihi soal yang tersedia'); \Log::error('Detail: Level ' . $level . ' - Setting: ' . $value . ', Tersedia: ' . $availableInLevel); return redirect()->back()->with('validation_error', [ 'title' => 'Error Validasi', 'message' => "Level $level: Setting $value soal, tapi hanya ada $availableInLevel soal tersedia.

POTENSI BUG: Sistem akan stuck karena tidak ada cukup soal di level tersebut.
CATATAN: Kurangi jumlah soal setting atau tambah soal di level tersebut." ]); } } // 4. VALIDASI: Batas naik level tidak boleh melebihi jumlah soal di level tersebut foreach ($request->batas_naik_level as $key => $value) { $level = str_replace('fase', '', $key); $soalInLevel = $request->jumlah_soal_per_level["level$level"] ?? 0; // VALIDASI BARU: Batas naik level tidak boleh sama atau lebih besar dari jumlah soal di level if ($value >= $soalInLevel) { return redirect()->back()->with('validation_error', [ 'title' => 'Error Validasi', 'message' => "Level $level: Syarat naik level ($value) tidak boleh sama atau lebih besar dari jumlah soal ($soalInLevel).

POTENSI BUG: Jika siswa salah satu saja, quiz akan stuck di level ini.
CATATAN: Kurangi syarat naik level atau tambah jumlah soal di level ini." ]); } if ($value > $soalInLevel) { \Log::error('Quiz Import Error: Batas naik level melebihi jumlah soal di level'); \Log::error('Detail: Level ' . $level . ' - Batas naik: ' . $value . ', Soal di level: ' . $soalInLevel); return redirect()->back()->with('validation_error', [ 'title' => 'Error Validasi', 'message' => "Level $level: Batas naik level ($value) tidak boleh melebihi jumlah soal ($soalInLevel).

POTENSI BUG: Siswa tidak akan pernah naik level karena batas terlalu tinggi.
CATATAN: Batas naik level harus ≤ jumlah soal di level tersebut." ]); } } // 5. VALIDASI: Pastikan ada soal di setiap level yang di-setting foreach ($request->jumlah_soal_per_level as $key => $value) { $level = str_replace('level', '', $key); $availableInLevel = $levelCounts[$level] ?? 0; if ($availableInLevel == 0) { \Log::error('Quiz Import Error: Tidak ada soal di level yang di-setting'); \Log::error('Detail: Level ' . $level . ' tidak memiliki soal di data import'); return redirect()->back()->with('validation_error', [ 'title' => 'Error Validasi', 'message' => "Level $level: Tidak ada soal tersedia di level ini.

POTENSI BUG: Sistem akan stuck karena tidak ada soal di level tersebut.
CATATAN: Pastikan data import memiliki soal dengan level $level atau hapus setting level ini." ]); } } // 6. VALIDASI: Level harus berurutan (1, 2, 3, dst) $levels = array_keys($request->jumlah_soal_per_level); sort($levels); $expectedLevels = []; for ($i = 1; $i <= count($levels); $i++) { $expectedLevels[] = "level$i"; } if ($levels !== $expectedLevels) { \Log::error('Quiz Import Error: Level tidak berurutan'); \Log::error('Detail: Level yang ada = ' . json_encode($levels) . ', Level yang diharapkan = ' . json_encode($expectedLevels)); return redirect()->back()->with('validation_error', [ 'title' => 'Error Validasi', 'message' => "Level harus berurutan (Level 1, Level 2, Level 3, dst).

POTENSI BUG: Sistem adaptive learning akan bingung dengan level yang tidak berurutan.
CATATAN: Pastikan level di data import berurutan dari 1, 2, 3, dst." ]); } // Update session dengan nilai total_soal_tampil yang baru Session::put('total_soal_tampil', $request->total_soal_tampil); // VALIDASI: Jumlah soal yang harus dikerjakan per level tidak boleh melebihi jumlah soal di bank soal $totalSoalPerLevel = 0; foreach ($request->jumlah_soal_per_level as $key => $value) { $level = str_replace('level', '', $key); $soalTersedia = $request->total_soal_per_level["level$level"] ?? 0; if ($value > $soalTersedia) { return redirect()->back()->with('validation_error', [ 'title' => 'Error Validasi', 'message' => "Level $level: Jumlah soal yang dikerjakan ($value) tidak boleh lebih besar dari jumlah soal di bank soal ($soalTersedia)." ]); } $totalSoalPerLevel += (int) $value; } // VALIDASI: Total soal yang harus dikerjakan (semua level) harus sama dengan total soal tampil if ($totalSoalPerLevel != $request->total_soal_tampil) { return redirect()->back()->with('validation_error', [ 'title' => 'Error Validasi', 'message' => "Total soal yang harus dikerjakan ($totalSoalPerLevel) tidak sama dengan total soal tampil ({$request->total_soal_tampil}).

POTENSI BUG: Quiz bisa stuck atau soal tidak cukup.
CATATAN: Sesuaikan jumlah soal per level agar totalnya sama dengan total soal tampil." ]); } // VALIDASI: Cegah quiz stuck jika syarat naik level tidak tercapai dan soal habis foreach ($request->batas_naik_level as $key => $value) { $level = str_replace('fase', '', $key); $jumlah_soal = $request->jumlah_soal_per_level["level$level"] ?? 0; $batas_naik = $value; // Jika syarat naik lebih besar dari jumlah soal, mustahil if ($batas_naik > $jumlah_soal) { return redirect()->back()->with('validation_error', [ 'title' => 'Error Validasi', 'message' => "Level $level: Syarat naik level ($batas_naik) tidak boleh lebih besar dari jumlah soal yang dikerjakan ($jumlah_soal)." ]); } // Jika syarat naik level terlalu rendah, tetap valid, tapi cek kemungkinan stuck // Simulasi: Jika siswa menjawab semua soal tapi benar kurang dari syarat naik, quiz stuck // Contoh: syarat naik 3 dari 10, jika siswa hanya benar 2, quiz stuck // Validasi: syarat naik level harus bisa dicapai dengan minimal 1 benar di setiap soal // (tidak perlu, karena sudah dicegah oleh logika di atas) // Namun, jika syarat naik terlalu rendah, warning saja (tidak error) // Jika syarat naik terlalu tinggi, error // Jika syarat naik level tidak tercapai dan soal habis, quiz stuck // (Sudah dicegah oleh validasi di atas) } // VALIDASI: Cegah quiz stuck jika siswa gagal naik level dan soal habis /* $levelKeys = array_keys($request->jumlah_soal_per_level); $levelCount = count($levelKeys); for ($i = 0; $i < $levelCount - 1; $i++) { // Kecuali level terakhir $currentLevelKey = $levelKeys[$i]; $nextLevelKeys = array_slice($levelKeys, $i + 1); $soalDiLevelIni = (int) $request->jumlah_soal_per_level[$currentLevelKey]; $soalDiLevelBerikutnya = 0; foreach ($nextLevelKeys as $k) { $soalDiLevelBerikutnya += (int) $request->jumlah_soal_per_level[$k]; } $totalSoalTampil = (int) $request->total_soal_tampil; $batasNaik = (int) ($request->batas_naik_level['fase' . ($i + 1)] ?? 0); // Jika semua soal di level ini habis, dan siswa gagal naik (benar < batas naik), quiz stuck // Kondisi: soal di level ini = total soal tampil - soal di level berikutnya if ($soalDiLevelIni === $totalSoalTampil - $soalDiLevelBerikutnya && $soalDiLevelIni > 0 && $batasNaik > 0) { return redirect()->back()->with('validation_error', [ 'title' => 'Konfigurasi Quiz Tidak Valid', 'message' => "Quiz tidak bisa disimpan karena pada fase " . ($i + 1) . ", siswa yang gagal naik ke level berikutnya akan kehabisan soal. Mohon ulangi konfigurasi quiz agar lebih seimbang dan baik." ]); } } */ try { // Simpan quiz dan soalnya ke DB $quiz = Quizzes::create([ 'judul' => $request->judul, 'deskripsi' => $request->deskripsi, 'matapelajaran_id' => $request->matapelajaran_id, 'total_soal' => $request->total_soal, 'total_soal_tampil' => $request->total_soal_tampil, 'waktu' => $request->waktu, ]); QuizLevelSetting::create([ 'quiz_id' => $quiz->id, 'jumlah_soal_per_level' => json_encode($request->jumlah_soal_per_level), 'level_awal' => session('level_awal') ?? 1, 'batas_naik_level' => json_encode($request->batas_naik_level), 'skor_level' => json_encode(session('skor_level')), 'kkm' => session('kkm') ?? 75, ]); // Menampung data dalam array sebelum disimpan $quizQuestionsData = []; foreach ($preview as $row) { $jawabanBenar = strtolower(trim($row[2])); // Menyiapkan data untuk disimpan $quizQuestionsData[] = [ 'quiz_id' => $quiz->id, 'pertanyaan' => $row[1], 'opsi_a' => $row[4], 'opsi_b' => $row[5], 'opsi_c' => $row[6], 'opsi_d' => $row[7], 'jawaban_benar' => $jawabanBenar, 'level' => $row[3], 'skor' => $row[8], 'created_at' => now(), 'updated_at' => now(), ]; } // Simpan semua data sekali gus menggunakan insert if (!empty($quizQuestionsData)) { QuizQuestions::insert($quizQuestionsData); } // Hapus session $this->removeSession(); $matpel = MataPelajaran::findOrFail($request->matapelajaran_id); // Cari siswa berdasarkan kelas dan tahun ajaran $siswas = Siswa::where('kelas', $matpel['kelas']) ->where('tahun_ajaran', $matpel['tahun_ajaran']) ->get(); // Kirim notifikasi ke setiap siswa foreach ($siswas as $siswa) { $siswa->notify(new QuizBaruNotification($quiz)); } \Log::info('Quiz berhasil diimport: ' . $request->judul); return redirect()->route('quiz')->with('success_message', [ 'title' => 'Berhasil', 'message' => 'Data quiz berhasil disimpan dan notifikasi telah dikirim ke siswa.' ]); } catch (\Exception $e) { \Log::error('Quiz Import Error: ' . $e->getMessage()); return redirect()->back()->with('validation_error', [ 'title' => 'Terjadi Kesalahan', 'message' => 'Gagal menyimpan data quiz: ' . $e->getMessage() ]); } catch (\Illuminate\Database\QueryException $e) { \Log::error('Quiz Import Error: Database error - ' . $e->getMessage()); return redirect()->back()->with('validation_error', [ 'title' => 'Terjadi Kesalahan', 'message' => 'Gagal menyimpan data quiz. Silakan coba lagi.' ]); } } /** * Display the specified resource. */ public function show(string $id) { // } /** * Show the form for editing the specified resource. */ public function edit(string $id) { // } /** * Update the specified resource in storage. */ public function update(Request $request, string $id) { // } /** * Remove the specified resource from storage. */ public function destroy(Request $request) { try { $quiz = Quizzes::findOrFail($request->formid); $quiz->delete(); return redirect()->back()->with('success_message', [ 'title' => 'Berhasil', 'message' => 'Data quiz berhasil dihapus.' ]); } catch (\Throwable $th) { return redirect()->back()->with('validation_error', [ 'title' => 'Terjadi Kesalahan', 'message' => 'Gagal menghapus data quiz: ' . $th->getMessage() ]); } } }