This commit is contained in:
RetasyaSalsabila 2026-04-05 11:29:32 +07:00
parent d06fafb7cc
commit f84402aff7
10 changed files with 212 additions and 28 deletions

View File

@ -6,6 +6,7 @@
use App\Models\Challenge; use App\Models\Challenge;
use App\Models\PesertaChallenge; use App\Models\PesertaChallenge;
use App\Models\Leaderboard; use App\Models\Leaderboard;
use App\Services\BadgeService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Carbon\Carbon; use Carbon\Carbon;
@ -101,7 +102,6 @@ public function submit(Request $request, $id_challenge)
} }
} }
// Semester & tahun ajaran otomatis
$now = Carbon::now(); $now = Carbon::now();
$semester = $now->month >= 7 ? '1' : '2'; $semester = $now->month >= 7 ? '1' : '2';
$tahunAjaran = $now->month >= 7 $tahunAjaran = $now->month >= 7
@ -118,7 +118,6 @@ public function submit(Request $request, $id_challenge)
'status' => 'selesai', 'status' => 'selesai',
]); ]);
// firstOrCreate dengan semester & tahun_ajaran sebagai key
$lb = Leaderboard::firstOrCreate( $lb = Leaderboard::firstOrCreate(
[ [
'id_siswa' => $siswa->id_siswa, 'id_siswa' => $siswa->id_siswa,
@ -131,7 +130,6 @@ public function submit(Request $request, $id_challenge)
$lb->increment('total_exp', $totalExp); $lb->increment('total_exp', $totalExp);
// Recalculate ranking per kelas + semester + tahun ajaran
Leaderboard::where('id_kelas', $siswa->id_kelas) Leaderboard::where('id_kelas', $siswa->id_kelas)
->where('semester', $semester) ->where('semester', $semester)
->where('tahun_ajaran', $tahunAjaran) ->where('tahun_ajaran', $tahunAjaran)
@ -140,6 +138,9 @@ public function submit(Request $request, $id_challenge)
->each(fn($row, $i) => $row->update(['ranking' => $i + 1])); ->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); return redirect()->route('siswa.challenge.hasil', $id_challenge);
} }

View File

@ -4,6 +4,7 @@
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Leaderboard; use App\Models\Leaderboard;
use App\Services\BadgeService;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Carbon\Carbon; use Carbon\Carbon;
@ -41,19 +42,34 @@ private function getData()
$myRank = $leaderboard->firstWhere('id_siswa', $siswa->id_siswa); $myRank = $leaderboard->firstWhere('id_siswa', $siswa->id_siswa);
return compact('leaderboard', 'myRank', 'semester', 'tahunAjaran'); return compact('leaderboard', 'myRank', 'semester', 'tahunAjaran', 'siswa');
} }
public function index() public function index()
{ {
$data = $this->getData(); $data = $this->getData();
// Keluarkan 'siswa' dari data yang dikirim ke view (tidak dibutuhkan di view)
unset($data['siswa']);
return view('siswa.leaderboard.index', $data); 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() 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); return response()->json($data);
} }
} }

View File

@ -9,6 +9,7 @@
use App\Models\Tugas; use App\Models\Tugas;
use App\Models\PengumpulanTugas; use App\Models\PengumpulanTugas;
use App\Models\Mengajar; use App\Models\Mengajar;
use App\Services\BadgeService;
class TugasController extends Controller class TugasController extends Controller
{ {
@ -24,7 +25,6 @@ public function index()
{ {
$siswa = Auth::guard('siswa')->user(); $siswa = Auth::guard('siswa')->user();
// Ambil semua tugas untuk kelas siswa
$semuaTugas = Tugas::with(['mengajar.mapel', 'mengajar.guru']) $semuaTugas = Tugas::with(['mengajar.mapel', 'mengajar.guru'])
->whereHas('mengajar', function ($q) use ($siswa) { ->whereHas('mengajar', function ($q) use ($siswa) {
$q->where('id_kelas', $siswa->id_kelas); $q->where('id_kelas', $siswa->id_kelas);
@ -32,39 +32,37 @@ public function index()
->orderBy('deadline', 'asc') ->orderBy('deadline', 'asc')
->get(); ->get();
// Tandai status tiap tugas untuk siswa ini
$tugasList = $semuaTugas->map(function ($tugas) use ($siswa) { $tugasList = $semuaTugas->map(function ($tugas) use ($siswa) {
$pengumpulan = PengumpulanTugas::where('id_tugas', $tugas->id_tugas) $pengumpulan = PengumpulanTugas::where('id_tugas', $tugas->id_tugas)
->where('id_siswa', $siswa->id_siswa) ->where('id_siswa', $siswa->id_siswa)
->first(); ->first();
$now = Carbon::now(); $now = Carbon::now();
$deadline = Carbon::parse($tugas->deadline); $deadline = Carbon::parse($tugas->deadline);
if ($pengumpulan) { if ($pengumpulan) {
$status = $pengumpulan->status; // 'dikumpulkan' atau 'terlambat' $status = $pengumpulan->status;
} else { } else {
$status = $now->greaterThan($deadline) ? 'terlambat' : 'belum'; $status = $now->greaterThan($deadline) ? 'terlambat' : 'belum';
} }
return [ return [
'id_tugas' => $tugas->id_tugas, 'id_tugas' => $tugas->id_tugas,
'judul' => $tugas->judul_tugas, 'judul' => $tugas->judul_tugas,
'keterangan' => $tugas->keterangan, 'keterangan' => $tugas->keterangan,
'deadline' => $deadline, 'deadline' => $deadline,
'nama_mapel' => optional(optional($tugas->mengajar)->mapel)->nama_mapel ?? '-', 'nama_mapel' => optional(optional($tugas->mengajar)->mapel)->nama_mapel ?? '-',
'nama_guru' => optional(optional($tugas->mengajar)->guru)->nama ?? '-', 'nama_guru' => optional(optional($tugas->mengajar)->guru)->nama ?? '-',
'status' => $status, 'status' => $status,
'sudah_kumpul'=> !is_null($pengumpulan), 'sudah_kumpul' => !is_null($pengumpulan),
'lampiran' => $pengumpulan?->lampiran_tugas, 'lampiran' => $pengumpulan?->lampiran_tugas,
'exp' => $pengumpulan?->exp ?? 0, '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'); $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')); 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.', 'lampiran_tugas.max' => 'Ukuran file maksimal 5MB.',
]); ]);
// Cek tugas valid untuk kelas siswa
$tugas = Tugas::whereHas('mengajar', function ($q) use ($siswa) { $tugas = Tugas::whereHas('mengajar', function ($q) use ($siswa) {
$q->where('id_kelas', $siswa->id_kelas); $q->where('id_kelas', $siswa->id_kelas);
}) })
->where('id_tugas', $id_tugas) ->where('id_tugas', $id_tugas)
->firstOrFail(); ->firstOrFail();
// Cek sudah dikumpulkan belum
$sudahAda = PengumpulanTugas::where('id_tugas', $id_tugas) $sudahAda = PengumpulanTugas::where('id_tugas', $id_tugas)
->where('id_siswa', $siswa->id_siswa) ->where('id_siswa', $siswa->id_siswa)
->exists(); ->exists();
@ -124,12 +120,10 @@ public function submit(Request $request, $id_tugas)
return back()->with('error', 'Kamu sudah mengumpulkan tugas ini.'); return back()->with('error', 'Kamu sudah mengumpulkan tugas ini.');
} }
// Upload file
$file = $request->file('lampiran_tugas'); $file = $request->file('lampiran_tugas');
$filename = 'tugas_' . $siswa->id_siswa . '_' . $id_tugas . '_' . time() . '.' . $file->getClientOriginalExtension(); $filename = 'tugas_' . $siswa->id_siswa . '_' . $id_tugas . '_' . time() . '.' . $file->getClientOriginalExtension();
$path = $file->storeAs('pengumpulan_tugas', $filename, 'public'); $path = $file->storeAs('pengumpulan_tugas', $filename, 'public');
// Tentukan status: terlambat atau dikumpulkan
$now = Carbon::now(); $now = Carbon::now();
$deadline = Carbon::parse($tugas->deadline); $deadline = Carbon::parse($tugas->deadline);
$status = $now->greaterThan($deadline) ? 'terlambat' : 'dikumpulkan'; $status = $now->greaterThan($deadline) ? 'terlambat' : 'dikumpulkan';
@ -139,10 +133,15 @@ public function submit(Request $request, $id_tugas)
'id_siswa' => $siswa->id_siswa, 'id_siswa' => $siswa->id_siswa,
'lampiran_tugas' => $path, 'lampiran_tugas' => $path,
'tanggal_submit' => $now, 'tanggal_submit' => $now,
'exp' => 0, // exp diberikan guru saat menilai 'exp' => 0,
'status' => $status, 'status' => $status,
]); ]);
// --- Cek & berikan badge tugas hanya jika tepat waktu ---
if ($status === 'dikumpulkan') {
app(BadgeService::class)->checkTugasBadges($siswa->id_siswa);
}
$pesan = $status === 'terlambat' $pesan = $status === 'terlambat'
? 'Tugas berhasil dikumpulkan (terlambat).' ? 'Tugas berhasil dikumpulkan (terlambat).'
: 'Tugas berhasil dikumpulkan! 🎉'; : 'Tugas berhasil dikumpulkan! 🎉';

View File

@ -0,0 +1,168 @@
<?php
namespace App\Services;
use App\Models\Badge;
use App\Models\SiswaBadge;
use App\Models\PesertaChallenge;
use App\Models\PengumpulanTugas;
use App\Models\Leaderboard;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
class BadgeService
{
/**
* Syarat (kolom `syarat` di tabel badges) yang dikenali sistem.
* Nilai ini harus sama persis dengan isi kolom `syarat` di DB.
*
* CHALLENGE
* challenge_1 selesaikan 1 challenge
* challenge_3 selesaikan 3 challenge
*
* TUGAS
* tugas_1 kumpulkan 1 tugas (tepat waktu)
* tugas_3 kumpulkan 3 tugas (tepat waktu)
*
* LEADERBOARD (dicek & dicabut secara real-time)
* leaderboard_top5 masuk top 5 leaderboard kelas aktif
* leaderboard_top1 raih peringkat 1 leaderboard kelas aktif
*/
// -------------------------------------------------------------------------
// ENTRY POINTS
// -------------------------------------------------------------------------
/**
* Dipanggil setelah siswa submit challenge.
* Mengecek & memberikan badge challenge.
*/
public function checkChallengeBadges(int $idSiswa): void
{
$jumlahSelesai = PesertaChallenge::where('id_siswa', $idSiswa)
->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];
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB