diff --git a/app/Http/Controllers/Admin/ChallengeController.php b/app/Http/Controllers/Admin/ChallengeController.php index b93ad44..e710d5c 100644 --- a/app/Http/Controllers/Admin/ChallengeController.php +++ b/app/Http/Controllers/Admin/ChallengeController.php @@ -3,99 +3,185 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; -use Illuminate\Http\Request; use App\Models\Challenge; -use App\Models\Kelas; use App\Models\SoalChallenge; +use App\Models\Kelas; +use App\Models\Badge; +use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; - class ChallengeController extends Controller { - public function index() + public function index(Request $request) { - $challenges = Challenge::latest()->get(); - $kelass = Kelas::all(); - return view('admin.challenge.index', compact('challenges', 'kelass')); - } + $query = Challenge::with(['kelas', 'soal']) + ->withCount('soal'); - public function create() - { - return view('admin.challenge.create'); - } - -public function store(Request $request) -{ - $request->validate([ - 'judul_challenge' => 'required', - 'exp' => 'required|integer|min:1', - 'tenggat_waktu' => 'required|date', - 'kelas' => 'required|array', - 'pertanyaan' => 'required|array|min:1' - ]); - - DB::transaction(function () use ($request) { - - $challenge = Challenge::create([ - 'id_admin' => auth('admin')->id(), - 'judul_challenge' => $request->judul_challenge, - 'deskripsi' => $request->deskripsi, - 'exp' => $request->exp, - 'tenggat_waktu' => $request->tenggat_waktu, - ]); - - $challenge->kelas()->attach($request->kelas); - - $jumlahSoal = count($request->pertanyaan); - $expPerSoal = floor($request->exp / $jumlahSoal); - - foreach ($request->pertanyaan as $i => $pertanyaan) { - SoalChallenge::create([ - 'id_challenge' => $challenge->id_challenge, - 'pertanyaan' => $pertanyaan, - 'opsi_a' => $request->opsi_a[$i], - 'opsi_b' => $request->opsi_b[$i], - 'opsi_c' => $request->opsi_c[$i], - 'opsi_d' => $request->opsi_d[$i], - 'jawaban_benar' => $request->jawaban_benar[$i], - 'exp_per_soal' => $expPerSoal, - ]); + if ($request->filled('search')) { + $query->where('judul_challenge', 'like', '%' . $request->search . '%'); } - }); + $challenges = $query->orderBy('created_at', 'desc') + ->paginate(10) + ->appends($request->all()); - return redirect()->route('admin.challenge.index') - ->with('success', 'Challenge & soal berhasil dibuat!'); -} + $kelas = Kelas::orderBy('tingkat')->orderBy('nama_kelas')->get(); + $badges = Badge::all(); + + return view('admin.challenge.index', compact('challenges', 'kelas', 'badges')); + } + + public function store(Request $request) + { + $request->validate([ + 'judul_challenge' => 'required|string|max:200', + 'deskripsi' => 'nullable|string', + 'exp' => 'required|integer|min:0', + 'id_badge' => 'nullable|exists:badges,id_badge', + 'tenggat_waktu' => 'required|date|after:now', + 'id_kelas' => 'required|array|min:1', + 'id_kelas.*' => 'exists:kelas,id_kelas', + // Soal + 'pertanyaan' => 'required|array|min:1', + 'pertanyaan.*' => 'required|string', + 'opsi_a.*' => 'required|string', + 'opsi_b.*' => 'required|string', + 'opsi_c.*' => 'required|string', + 'opsi_d.*' => 'required|string', + 'jawaban_benar.*' => 'required|in:A,B,C,D', + 'exp_per_soal.*' => 'required|integer|min:0', + ], [ + 'tenggat_waktu.after' => 'Tenggat waktu harus lebih dari sekarang.', + 'pertanyaan.required' => 'Minimal harus ada 1 soal.', + 'id_kelas.required' => 'Pilih minimal 1 kelas.', + ]); + + DB::transaction(function () use ($request) { + $admin = Auth::guard('admin')->user(); + + $challenge = Challenge::create([ + 'id_admin' => $admin->id_admin, + 'judul_challenge' => $request->judul_challenge, + 'deskripsi' => $request->deskripsi, + 'exp' => $request->exp, + 'id_badge' => $request->id_badge, + 'tenggat_waktu' => $request->tenggat_waktu, + ]); + + // Attach ke kelas + $challenge->kelas()->sync($request->id_kelas); + + // Simpan soal + foreach ($request->pertanyaan as $i => $pertanyaan) { + SoalChallenge::create([ + 'id_challenge' => $challenge->id_challenge, + 'pertanyaan' => $pertanyaan, + 'opsi_a' => $request->opsi_a[$i], + 'opsi_b' => $request->opsi_b[$i], + 'opsi_c' => $request->opsi_c[$i], + 'opsi_d' => $request->opsi_d[$i], + 'jawaban_benar' => $request->jawaban_benar[$i], + 'exp_per_soal' => $request->exp_per_soal[$i], + ]); + } + }); + + return redirect()->route('admin.challenge.index') + ->with('success', 'Challenge berhasil dibuat!'); + } + + public function show($id) + { + $challenge = Challenge::with(['kelas', 'soal'])->findOrFail($id); + return view('admin.challenge.show', compact('challenge')); + } public function edit($id) { - $challenge = Challenge::findOrFail($id); - return view('admin.challenge.edit', compact('challenge')); + $challenge = Challenge::with(['kelas', 'soal'])->findOrFail($id); + $kelas = Kelas::orderBy('tingkat')->orderBy('nama_kelas')->get(); + $badges = Badge::all(); + return view('admin.challenge.edit', compact('challenge', 'kelas', 'badges')); } public function update(Request $request, $id) { $challenge = Challenge::findOrFail($id); - $challenge->update([ - 'judul_challenge' => $request->judul_challenge, - 'deskripsi' => $request->deskripsi, - 'exp' => $request->exp, - 'tenggat_waktu' => $request->tenggat_waktu, + $request->validate([ + 'judul_challenge' => 'required|string|max:200', + 'deskripsi' => 'nullable|string', + 'exp' => 'required|integer|min:0', + 'id_badge' => 'nullable|exists:badges,id_badge', + 'tenggat_waktu' => 'required|date', + 'id_kelas' => 'required|array|min:1', + 'id_kelas.*' => 'exists:kelas,id_kelas', + 'pertanyaan' => 'required|array|min:1', + 'pertanyaan.*' => 'required|string', + 'opsi_a.*' => 'required|string', + 'opsi_b.*' => 'required|string', + 'opsi_c.*' => 'required|string', + 'opsi_d.*' => 'required|string', + 'jawaban_benar.*' => 'required|in:A,B,C,D', + 'exp_per_soal.*' => 'required|integer|min:0', ]); + DB::transaction(function () use ($request, $challenge) { + $challenge->update([ + 'judul_challenge' => $request->judul_challenge, + 'deskripsi' => $request->deskripsi, + 'exp' => $request->exp, + 'id_badge' => $request->id_badge, + 'tenggat_waktu' => $request->tenggat_waktu, + ]); + + $challenge->kelas()->sync($request->id_kelas); + + // Hapus soal lama, insert ulang + SoalChallenge::where('id_challenge', $challenge->id_challenge)->delete(); + + foreach ($request->pertanyaan as $i => $pertanyaan) { + SoalChallenge::create([ + 'id_challenge' => $challenge->id_challenge, + 'pertanyaan' => $pertanyaan, + 'opsi_a' => $request->opsi_a[$i], + 'opsi_b' => $request->opsi_b[$i], + 'opsi_c' => $request->opsi_c[$i], + 'opsi_d' => $request->opsi_d[$i], + 'jawaban_benar' => $request->jawaban_benar[$i], + 'exp_per_soal' => $request->exp_per_soal[$i], + ]); + } + }); + return redirect()->route('admin.challenge.index') ->with('success', 'Challenge berhasil diupdate!'); } public function destroy($id) { - $challenge = Challenge::findOrFail($id); - $challenge->delete(); + Challenge::findOrFail($id)->delete(); return redirect()->route('admin.challenge.index') - ->with('success', 'Challenge berhasil dihapus!'); + ->with('success', 'Challenge berhasil dihapus.'); } -} + + /** + * AJAX โ return data challenge untuk modal edit + */ + public function editData($id) + { + $challenge = Challenge::with(['kelas', 'soal'])->findOrFail($id); + + return response()->json([ + 'judul_challenge' => $challenge->judul_challenge, + 'deskripsi' => $challenge->deskripsi, + 'exp' => $challenge->exp, + 'id_badge' => $challenge->id_badge, + 'tenggat_waktu' => $challenge->tenggat_waktu, + 'kelas' => $challenge->kelas->pluck('id_kelas'), + 'soal' => $challenge->soal, + ]); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Siswa/ChallengeController.php b/app/Http/Controllers/Siswa/ChallengeController.php new file mode 100644 index 0000000..20f14ef --- /dev/null +++ b/app/Http/Controllers/Siswa/ChallengeController.php @@ -0,0 +1,172 @@ +user(); + + $challenges = Challenge::whereHas('kelas', function ($q) use ($siswa) { + $q->where('challenge_kelas.id_kelas', $siswa->id_kelas); + }) + ->with(['soal']) + ->withCount('soal') + ->orderBy('tenggat_waktu', 'asc') + ->get(); + + $sudahDikerjakan = PesertaChallenge::where('id_siswa', $siswa->id_siswa) + ->where('status', 'selesai') + ->pluck('id_challenge') + ->toArray(); + + return view('siswa.challenge.index', compact('challenges', 'sudahDikerjakan')); + } + + public function kerjakan($id_challenge) + { + $siswa = Auth::guard('siswa')->user(); + + $challenge = Challenge::whereHas('kelas', function ($q) use ($siswa) { + $q->where('challenge_kelas.id_kelas', $siswa->id_kelas); + }) + ->with('soal') + ->findOrFail($id_challenge); + + $sudah = PesertaChallenge::where('id_siswa', $siswa->id_siswa) + ->where('id_challenge', $id_challenge) + ->where('status', 'selesai') + ->exists(); + + if ($sudah) { + return redirect()->route('siswa.challenge.hasil', $id_challenge); + } + + if (Carbon::parse($challenge->tenggat_waktu)->isPast()) { + return redirect()->route('siswa.challenge.index') + ->with('error', 'Challenge ini sudah melewati tenggat waktu.'); + } + + if ($challenge->soal->isEmpty()) { + return redirect()->route('siswa.challenge.index') + ->with('error', 'Challenge ini belum memiliki soal.'); + } + + return view('siswa.challenge.kerjakan', compact('challenge')); + } + + public function submit(Request $request, $id_challenge) + { + $siswa = Auth::guard('siswa')->user(); + + $challenge = Challenge::whereHas('kelas', function ($q) use ($siswa) { + $q->where('challenge_kelas.id_kelas', $siswa->id_kelas); + }) + ->with('soal') + ->findOrFail($id_challenge); + + $sudah = PesertaChallenge::where('id_siswa', $siswa->id_siswa) + ->where('id_challenge', $id_challenge) + ->where('status', 'selesai') + ->exists(); + + if ($sudah) { + return redirect()->route('siswa.challenge.hasil', $id_challenge); + } + + if (Carbon::parse($challenge->tenggat_waktu)->isPast()) { + return redirect()->route('siswa.challenge.index') + ->with('error', 'Challenge sudah melewati tenggat waktu.'); + } + + $jawaban = $request->input('jawaban', []); + $totalExp = 0; + $jawabanJson = []; + + foreach ($challenge->soal as $soal) { + $jwb = $jawaban[$soal->id_soal] ?? null; + $jawabanJson[$soal->id_soal] = $jwb; + if ($jwb && strtoupper($jwb) === strtoupper($soal->jawaban_benar)) { + $totalExp += $soal->exp_per_soal; + } + } + + // Semester & tahun ajaran otomatis + $now = Carbon::now(); + $semester = $now->month >= 7 ? '1' : '2'; + $tahunAjaran = $now->month >= 7 + ? $now->year . '/' . ($now->year + 1) + : ($now->year - 1) . '/' . $now->year; + + DB::transaction(function () use ($siswa, $challenge, $jawabanJson, $totalExp, $semester, $tahunAjaran) { + PesertaChallenge::create([ + 'id_challenge' => $challenge->id_challenge, + 'id_siswa' => $siswa->id_siswa, + 'jawaban' => json_encode($jawabanJson), + 'waktu_submit' => Carbon::now(), + 'exp' => $totalExp, + 'status' => 'selesai', + ]); + + // firstOrCreate dengan semester & tahun_ajaran sebagai key + $lb = Leaderboard::firstOrCreate( + [ + 'id_siswa' => $siswa->id_siswa, + 'id_kelas' => $siswa->id_kelas, + 'semester' => $semester, + 'tahun_ajaran' => $tahunAjaran, + ], + ['total_exp' => 0, 'ranking' => 0] + ); + + $lb->increment('total_exp', $totalExp); + + // Recalculate ranking per kelas + semester + tahun ajaran + Leaderboard::where('id_kelas', $siswa->id_kelas) + ->where('semester', $semester) + ->where('tahun_ajaran', $tahunAjaran) + ->orderBy('total_exp', 'desc') + ->get() + ->each(fn($row, $i) => $row->update(['ranking' => $i + 1])); + }); + + return redirect()->route('siswa.challenge.hasil', $id_challenge); + } + + public function hasil($id_challenge) + { + $siswa = Auth::guard('siswa')->user(); + + $challenge = Challenge::with('soal')->findOrFail($id_challenge); + + $peserta = PesertaChallenge::where('id_siswa', $siswa->id_siswa) + ->where('id_challenge', $id_challenge) + ->firstOrFail(); + + $jawabanSiswa = json_decode($peserta->jawaban, true) ?? []; + + $benar = 0; $salah = 0; + foreach ($challenge->soal as $soal) { + $jwb = $jawabanSiswa[$soal->id_soal] ?? null; + strtoupper((string)$jwb) === strtoupper($soal->jawaban_benar) ? $benar++ : $salah++; + } + + $totalSoal = $challenge->soal->count(); + $persentase = $totalSoal > 0 ? round(($benar / $totalSoal) * 100) : 0; + + return view('siswa.challenge.hasil', compact( + 'challenge', 'peserta', 'jawabanSiswa', + 'benar', 'salah', 'totalSoal', 'persentase' + )); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Siswa/LeaderboardController.php b/app/Http/Controllers/Siswa/LeaderboardController.php new file mode 100644 index 0000000..8f437dd --- /dev/null +++ b/app/Http/Controllers/Siswa/LeaderboardController.php @@ -0,0 +1,46 @@ +user(); + + $now = Carbon::now(); + $semester = $now->month >= 7 ? '1' : '2'; + $tahunAjaran = $now->month >= 7 + ? $now->year . '/' . ($now->year + 1) + : ($now->year - 1) . '/' . $now->year; + + // Top leaderboard kelas siswa ini + $leaderboard = Leaderboard::with('siswa') + ->where('id_kelas', $siswa->id_kelas) + ->where('semester', $semester) + ->where('tahun_ajaran', $tahunAjaran) + ->orderBy('total_exp', 'desc') + ->get() + ->map(function ($item, $i) { + return [ + 'ranking' => $i + 1, + 'nama' => optional($item->siswa)->nama ?? '-', + 'nisn' => optional($item->siswa)->nisn ?? '-', + 'exp' => $item->total_exp, + 'id_siswa'=> $item->id_siswa, + ]; + }); + + // Posisi siswa yang login + $myRank = $leaderboard->firstWhere('id_siswa', $siswa->id_siswa); + + return view('siswa.leaderboard.index', compact( + 'leaderboard', 'myRank', 'semester', 'tahunAjaran' + )); + } +} \ No newline at end of file diff --git a/database/migrations/2026_03_11_091158_fix_leaderboards.php b/database/migrations/2026_03_11_091158_fix_leaderboards.php new file mode 100644 index 0000000..9a94971 --- /dev/null +++ b/database/migrations/2026_03_11_091158_fix_leaderboards.php @@ -0,0 +1,26 @@ +integer('ranking')->default(0)->change(); + $table->string('semester', 50)->default('1')->change(); + $table->string('tahun_ajaran', 20)->default('2024/2025')->change(); + }); + } + + public function down(): void + { + Schema::table('leaderboards', function (Blueprint $table) { + $table->integer('ranking')->default(null)->change(); + $table->string('semester', 50)->default(null)->change(); + $table->string('tahun_ajaran', 20)->default(null)->change(); + }); + } +}; \ No newline at end of file diff --git a/resources/views/admin/challenge/create.blade.php b/resources/views/admin/challenge/create.blade.php deleted file mode 100644 index 05b11f4..0000000 --- a/resources/views/admin/challenge/create.blade.php +++ /dev/null @@ -1,23 +0,0 @@ -@extends('admin.layouts.app') - -@section('content') -
| No | Judul Challenge | -EXP | +Kelas | +Soal | +Total EXP | Tenggat | Aksi | ||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| {{ $loop->iteration }} | -{{ $challenge->judul_challenge }} | -{{ $challenge->exp }} | -{{ \Carbon\Carbon::parse($challenge->tenggat_waktu)->format('d M Y H:i') }} | - +{{ $challenges->firstItem() + $i }} | +
+ {{ $ch->judul_challenge }}
+ @if($ch->deskripsi)
+ {{ Str::limit($ch->deskripsi, 50) }}
+ @endif
+ |
- |
+ {{ $ch->soal_count }} soal | +{{ $ch->exp }} EXP | +
+
+ {{ $isLewat ? 'โฐ Lewat' : 'โ
Aktif' }}
+
+
+ {{ \Carbon\Carbon::parse($ch->tenggat_waktu)->format('d M Y, H:i') }}
+
+ |
+
+ | |
| Belum ada challenge | +Belum ada challenge. | ||||||||||