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]; } }