badge
This commit is contained in:
parent
d06fafb7cc
commit
f84402aff7
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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! 🎉';
|
||||||
|
|
|
||||||
|
|
@ -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 |
Loading…
Reference in New Issue