diff --git a/app/Http/Controllers/Siswa/ChallengeController.php b/app/Http/Controllers/Siswa/ChallengeController.php index 20f14ef..b62da20 100644 --- a/app/Http/Controllers/Siswa/ChallengeController.php +++ b/app/Http/Controllers/Siswa/ChallengeController.php @@ -6,6 +6,7 @@ use App\Models\Challenge; use App\Models\PesertaChallenge; use App\Models\Leaderboard; +use App\Services\BadgeService; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Carbon\Carbon; @@ -101,7 +102,6 @@ public function submit(Request $request, $id_challenge) } } - // Semester & tahun ajaran otomatis $now = Carbon::now(); $semester = $now->month >= 7 ? '1' : '2'; $tahunAjaran = $now->month >= 7 @@ -118,7 +118,6 @@ public function submit(Request $request, $id_challenge) 'status' => 'selesai', ]); - // firstOrCreate dengan semester & tahun_ajaran sebagai key $lb = Leaderboard::firstOrCreate( [ 'id_siswa' => $siswa->id_siswa, @@ -131,7 +130,6 @@ public function submit(Request $request, $id_challenge) $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) @@ -140,6 +138,9 @@ public function submit(Request $request, $id_challenge) ->each(fn($row, $i) => $row->update(['ranking' => $i + 1])); }); + // --- Cek & berikan badge challenge (di luar transaksi agar tidak block) --- + app(BadgeService::class)->checkChallengeBadges($siswa->id_siswa); + return redirect()->route('siswa.challenge.hasil', $id_challenge); } diff --git a/app/Http/Controllers/Siswa/LeaderboardController.php b/app/Http/Controllers/Siswa/LeaderboardController.php index d07582d..adb554f 100644 --- a/app/Http/Controllers/Siswa/LeaderboardController.php +++ b/app/Http/Controllers/Siswa/LeaderboardController.php @@ -4,6 +4,7 @@ use App\Http\Controllers\Controller; use App\Models\Leaderboard; +use App\Services\BadgeService; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Storage; use Carbon\Carbon; @@ -41,19 +42,34 @@ private function getData() $myRank = $leaderboard->firstWhere('id_siswa', $siswa->id_siswa); - return compact('leaderboard', 'myRank', 'semester', 'tahunAjaran'); + return compact('leaderboard', 'myRank', 'semester', 'tahunAjaran', 'siswa'); } public function index() { $data = $this->getData(); + // Keluarkan 'siswa' dari data yang dikirim ke view (tidak dibutuhkan di view) + unset($data['siswa']); return view('siswa.leaderboard.index', $data); } - // Endpoint JSON untuk polling real-time + /** + * Endpoint JSON untuk polling real-time. + * Setiap kali dipanggil, badge leaderboard siswa yang sedang login + * dievaluasi ulang (diberikan atau dicabut sesuai ranking saat ini). + */ public function json() { - $data = $this->getData(); + $data = $this->getData(); + $siswa = $data['siswa']; + + // Evaluasi badge leaderboard secara real-time untuk siswa yang sedang login + app(BadgeService::class)->checkLeaderboardBadges( + $siswa->id_siswa, + $siswa->id_kelas + ); + + unset($data['siswa']); return response()->json($data); } } \ No newline at end of file diff --git a/app/Http/Controllers/Siswa/TugasController.php b/app/Http/Controllers/Siswa/TugasController.php index 67463cf..f8e9b93 100644 --- a/app/Http/Controllers/Siswa/TugasController.php +++ b/app/Http/Controllers/Siswa/TugasController.php @@ -9,6 +9,7 @@ use App\Models\Tugas; use App\Models\PengumpulanTugas; use App\Models\Mengajar; +use App\Services\BadgeService; class TugasController extends Controller { @@ -24,7 +25,6 @@ public function index() { $siswa = Auth::guard('siswa')->user(); - // Ambil semua tugas untuk kelas siswa $semuaTugas = Tugas::with(['mengajar.mapel', 'mengajar.guru']) ->whereHas('mengajar', function ($q) use ($siswa) { $q->where('id_kelas', $siswa->id_kelas); @@ -32,39 +32,37 @@ public function index() ->orderBy('deadline', 'asc') ->get(); - // Tandai status tiap tugas untuk siswa ini $tugasList = $semuaTugas->map(function ($tugas) use ($siswa) { $pengumpulan = PengumpulanTugas::where('id_tugas', $tugas->id_tugas) ->where('id_siswa', $siswa->id_siswa) ->first(); - $now = Carbon::now(); + $now = Carbon::now(); $deadline = Carbon::parse($tugas->deadline); if ($pengumpulan) { - $status = $pengumpulan->status; // 'dikumpulkan' atau 'terlambat' + $status = $pengumpulan->status; } else { $status = $now->greaterThan($deadline) ? 'terlambat' : 'belum'; } return [ - 'id_tugas' => $tugas->id_tugas, - 'judul' => $tugas->judul_tugas, - 'keterangan' => $tugas->keterangan, - 'deadline' => $deadline, - 'nama_mapel' => optional(optional($tugas->mengajar)->mapel)->nama_mapel ?? '-', - 'nama_guru' => optional(optional($tugas->mengajar)->guru)->nama ?? '-', - 'status' => $status, - 'sudah_kumpul'=> !is_null($pengumpulan), - 'lampiran' => $pengumpulan?->lampiran_tugas, - 'exp' => $pengumpulan?->exp ?? 0, + 'id_tugas' => $tugas->id_tugas, + 'judul' => $tugas->judul_tugas, + 'keterangan' => $tugas->keterangan, + 'deadline' => $deadline, + 'nama_mapel' => optional(optional($tugas->mengajar)->mapel)->nama_mapel ?? '-', + 'nama_guru' => optional(optional($tugas->mengajar)->guru)->nama ?? '-', + 'status' => $status, + 'sudah_kumpul' => !is_null($pengumpulan), + 'lampiran' => $pengumpulan?->lampiran_tugas, + 'exp' => $pengumpulan?->exp ?? 0, ]; }); - // Kelompokkan: belum & pending vs sudah dikumpulkan - $tugasBelum = $tugasList->filter(fn($t) => !$t['sudah_kumpul'] && $t['status'] !== 'terlambat'); + $tugasBelum = $tugasList->filter(fn($t) => !$t['sudah_kumpul'] && $t['status'] !== 'terlambat'); $tugasTerlambat = $tugasList->filter(fn($t) => !$t['sudah_kumpul'] && $t['status'] === 'terlambat'); - $tugasSelesai = $tugasList->filter(fn($t) => $t['sudah_kumpul']); + $tugasSelesai = $tugasList->filter(fn($t) => $t['sudah_kumpul']); return view('siswa.tugas.index', compact('tugasBelum', 'tugasTerlambat', 'tugasSelesai')); } @@ -108,14 +106,12 @@ public function submit(Request $request, $id_tugas) 'lampiran_tugas.max' => 'Ukuran file maksimal 5MB.', ]); - // Cek tugas valid untuk kelas siswa $tugas = Tugas::whereHas('mengajar', function ($q) use ($siswa) { $q->where('id_kelas', $siswa->id_kelas); }) ->where('id_tugas', $id_tugas) ->firstOrFail(); - // Cek sudah dikumpulkan belum $sudahAda = PengumpulanTugas::where('id_tugas', $id_tugas) ->where('id_siswa', $siswa->id_siswa) ->exists(); @@ -124,12 +120,10 @@ public function submit(Request $request, $id_tugas) return back()->with('error', 'Kamu sudah mengumpulkan tugas ini.'); } - // Upload file $file = $request->file('lampiran_tugas'); $filename = 'tugas_' . $siswa->id_siswa . '_' . $id_tugas . '_' . time() . '.' . $file->getClientOriginalExtension(); $path = $file->storeAs('pengumpulan_tugas', $filename, 'public'); - // Tentukan status: terlambat atau dikumpulkan $now = Carbon::now(); $deadline = Carbon::parse($tugas->deadline); $status = $now->greaterThan($deadline) ? 'terlambat' : 'dikumpulkan'; @@ -139,10 +133,15 @@ public function submit(Request $request, $id_tugas) 'id_siswa' => $siswa->id_siswa, 'lampiran_tugas' => $path, 'tanggal_submit' => $now, - 'exp' => 0, // exp diberikan guru saat menilai + 'exp' => 0, 'status' => $status, ]); + // --- Cek & berikan badge tugas hanya jika tepat waktu --- + if ($status === 'dikumpulkan') { + app(BadgeService::class)->checkTugasBadges($siswa->id_siswa); + } + $pesan = $status === 'terlambat' ? 'Tugas berhasil dikumpulkan (terlambat).' : 'Tugas berhasil dikumpulkan! 🎉'; diff --git a/app/Services/BadgeService.php b/app/Services/BadgeService.php new file mode 100644 index 0000000..44e080a --- /dev/null +++ b/app/Services/BadgeService.php @@ -0,0 +1,168 @@ +where('status', 'selesai') + ->count(); + + $this->grantIfEligible($idSiswa, 'challenge_1', $jumlahSelesai >= 1); + $this->grantIfEligible($idSiswa, 'challenge_3', $jumlahSelesai >= 3); + } + + /** + * Dipanggil setelah siswa submit tugas. + * Mengecek & memberikan badge tugas (hanya status 'dikumpulkan' / tepat waktu). + */ + public function checkTugasBadges(int $idSiswa): void + { + $jumlahKumpul = PengumpulanTugas::where('id_siswa', $idSiswa) + ->where('status', 'dikumpulkan') + ->count(); + + $this->grantIfEligible($idSiswa, 'tugas_1', $jumlahKumpul >= 1); + $this->grantIfEligible($idSiswa, 'tugas_3', $jumlahKumpul >= 3); + } + + /** + * Dipanggil dari endpoint JSON leaderboard (polling real-time). + * Badge leaderboard DICABUT jika siswa tidak lagi memenuhi syarat, + * dan DIBERIKAN KEMBALI jika kembali memenuhi syarat. + * + * Hanya leaderboard semester & tahun ajaran aktif yang dievaluasi. + */ + public function checkLeaderboardBadges(int $idSiswa, int $idKelas): void + { + [$semester, $tahunAjaran] = $this->semesterAktif(); + + $lb = Leaderboard::where('id_siswa', $idSiswa) + ->where('id_kelas', $idKelas) + ->where('semester', $semester) + ->where('tahun_ajaran', $tahunAjaran) + ->first(); + + $ranking = $lb?->ranking ?? 0; + + // top5: ranking 1-5, top1: hanya ranking 1 + $isTop5 = $ranking >= 1 && $ranking <= 5; + $isTop1 = $ranking === 1; + + // Untuk badge leaderboard: grant jika eligible, revoke jika tidak + $this->grantOrRevoke($idSiswa, 'leaderboard_top5', $isTop5); + $this->grantOrRevoke($idSiswa, 'leaderboard_top1', $isTop1); + } + + // ------------------------------------------------------------------------- + // PRIVATE HELPERS + // ------------------------------------------------------------------------- + + /** + * Berikan badge jika eligible. Tidak melakukan apa-apa jika sudah punya. + * Digunakan untuk badge challenge & tugas (tidak pernah dicabut). + */ + private function grantIfEligible(int $idSiswa, string $syarat, bool $eligible): void + { + if (!$eligible) { + return; + } + + $badge = Badge::where('syarat', $syarat)->first(); + if (!$badge) { + return; // badge belum di-seed di DB, skip + } + + // Idempoten: cek dulu sebelum insert + $sudahPunya = SiswaBadge::where('id_siswa', $idSiswa) + ->where('id_badge', $badge->id_badge) + ->exists(); + + if (!$sudahPunya) { + SiswaBadge::create([ + 'id_siswa' => $idSiswa, + 'id_badge' => $badge->id_badge, + 'tanggal_diberikan' => Carbon::now(), + ]); + } + } + + /** + * Berikan badge jika eligible, CABUT jika tidak. + * Digunakan khusus untuk badge leaderboard (real-time). + */ + private function grantOrRevoke(int $idSiswa, string $syarat, bool $eligible): void + { + $badge = Badge::where('syarat', $syarat)->first(); + if (!$badge) { + return; + } + + $record = SiswaBadge::where('id_siswa', $idSiswa) + ->where('id_badge', $badge->id_badge) + ->first(); + + if ($eligible && !$record) { + // Berikan badge + SiswaBadge::create([ + 'id_siswa' => $idSiswa, + 'id_badge' => $badge->id_badge, + 'tanggal_diberikan' => Carbon::now(), + ]); + } elseif (!$eligible && $record) { + // Cabut badge + $record->delete(); + } + // Jika sudah punya & masih eligible, atau tidak punya & tidak eligible → tidak perlu apa-apa + } + + /** + * Mengembalikan [semester, tahun_ajaran] berdasarkan tanggal sekarang, + * konsisten dengan logika di ChallengeController. + */ + private function semesterAktif(): array + { + $now = Carbon::now(); + $semester = $now->month >= 7 ? '1' : '2'; + $tahunAjaran = $now->month >= 7 + ? $now->year . '/' . ($now->year + 1) + : ($now->year - 1) . '/' . $now->year; + + return [$semester, $tahunAjaran]; + } +} \ No newline at end of file diff --git a/public/images/badges/challenge_1.png b/public/images/badges/challenge_1.png new file mode 100644 index 0000000..bba8794 Binary files /dev/null and b/public/images/badges/challenge_1.png differ diff --git a/public/images/badges/challenge_3.png b/public/images/badges/challenge_3.png new file mode 100644 index 0000000..7d3d7a2 Binary files /dev/null and b/public/images/badges/challenge_3.png differ diff --git a/public/images/badges/top_1.png b/public/images/badges/top_1.png new file mode 100644 index 0000000..9530f56 Binary files /dev/null and b/public/images/badges/top_1.png differ diff --git a/public/images/badges/top_5.png b/public/images/badges/top_5.png new file mode 100644 index 0000000..2eaadca Binary files /dev/null and b/public/images/badges/top_5.png differ diff --git a/public/images/badges/tugas_1.png b/public/images/badges/tugas_1.png new file mode 100644 index 0000000..58995c6 Binary files /dev/null and b/public/images/badges/tugas_1.png differ diff --git a/public/images/badges/tugas_3.png b/public/images/badges/tugas_3.png new file mode 100644 index 0000000..d18c80a Binary files /dev/null and b/public/images/badges/tugas_3.png differ