From 9762288c384b629563bd36c7f447079d0f370fb2 Mon Sep 17 00:00:00 2001 From: Ryfandii <158023861+ryfandii@users.noreply.github.com> Date: Tue, 19 May 2026 12:08:41 +0700 Subject: [PATCH] final project bismillah lancar SIDANG --- .../admin/LaporanAkademikController.php | 42 ++ app/Http/Controllers/guru/NilaiController.php | 243 ++++--- app/Http/Controllers/guru/TugasController.php | 306 +++++---- .../Controllers/siswa/NilaiController.php | 153 +++-- app/Models/Nilai.php | 37 +- composer.json | 1 + composer.lock | 400 ++++++++++- ..._213247_add_sudah_kirim_to_nilai_table.php | 25 + public/uploads/1778813458.jpg | Bin 0 -> 9277 bytes public/uploads/1778814685.jpg | Bin 0 -> 9277 bytes resources/views/guru/nilai/edit.blade.php | 218 ++---- resources/views/guru/nilai/index.blade.php | 342 ++++++---- resources/views/guru/tugas/index.blade.php | 632 +++++++++++++----- resources/views/guru/ujian/index.blade.php | 20 +- resources/views/siswa/nilai/index.blade.php | 591 ++++------------ resources/views/siswa/tugas/index.blade.php | 4 +- routes/web.php | 40 +- 17 files changed, 1838 insertions(+), 1216 deletions(-) create mode 100644 app/Http/Controllers/admin/LaporanAkademikController.php create mode 100644 database/migrations/2026_05_09_213247_add_sudah_kirim_to_nilai_table.php create mode 100644 public/uploads/1778813458.jpg create mode 100644 public/uploads/1778814685.jpg diff --git a/app/Http/Controllers/admin/LaporanAkademikController.php b/app/Http/Controllers/admin/LaporanAkademikController.php new file mode 100644 index 0000000..b15e40f --- /dev/null +++ b/app/Http/Controllers/admin/LaporanAkademikController.php @@ -0,0 +1,42 @@ +orderBy('id_siswa') + ->get() + ->groupBy('id_siswa'); + + return view('admin.laporan-akademik.index', compact('nilai')); + } + + public function cetak($idSiswa) + { + $nilai = Nilai::with(['siswa', 'mapel']) + ->where('id_siswa', $idSiswa) + ->get(); + + if ($nilai->isEmpty()) { + abort(404, 'Data nilai tidak ditemukan.'); + } + + $siswa = $nilai->first()->siswa; + $rataRata = round($nilai->avg('nilai_akhir'), 2); + + $pdf = Pdf::loadView('admin.laporan-akademik.pdf', compact( + 'nilai', + 'siswa', + 'rataRata' + ))->setPaper('A4', 'portrait'); + + return $pdf->stream('laporan-akademik-' . $siswa->nama . '.pdf'); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/guru/NilaiController.php b/app/Http/Controllers/guru/NilaiController.php index 1ef8724..91805e0 100644 --- a/app/Http/Controllers/guru/NilaiController.php +++ b/app/Http/Controllers/guru/NilaiController.php @@ -1,6 +1,6 @@ user(); + { + $user = auth()->user(); + $mapel_id = $user->mapel_id; + $kelas = Kelas::all(); - // ambil mapel guru - $mapel_id = $user->mapel_id; + // Filter kelas + $query = Nilai::with(['siswa.kelas', 'mapel']) + ->where('mapel_id', $mapel_id); - // ambil semua kelas (buat dropdown filter) - $kelas = \App\Models\Kelas::all(); + if ($request->kelas_id) { + $query->whereHas('siswa', function ($q) use ($request) { + $q->where('kelas_id', $request->kelas_id); + }); + } - // query nilai + relasi siswa & mapel - $query = \App\Models\Nilai::with(['siswa.kelas', 'mapel']) - ->where('mapel_id', $mapel_id); + // Filter siswa (opsional) + if ($request->siswa_id) { + $query->where('siswa_id', $request->siswa_id); + } - // πŸ”₯ FILTER KELAS - if ($request->kelas_id) { - $query->whereHas('siswa', function ($q) use ($request) { - $q->where('kelas_id', $request->kelas_id); - }); + $nilai = $query->get(); + + // Siswa berdasarkan kelas yang dipilih (untuk dropdown filter siswa) + $siswaList = collect(); + if ($request->kelas_id) { + $siswaList = Siswa::where('kelas_id', $request->kelas_id)->get(); + } + + return view('guru.nilai.index', compact('nilai', 'kelas', 'siswaList')); } - $nilai = $query->get(); + // ── HITUNG RATA-RATA: simpan nilai_akhir ke kolom rata ────────── + public function hitungRata(Request $request) + { + $request->validate([ + 'kelas_id' => 'required', + ]); - return view('guru.nilai.index', compact('nilai', 'kelas')); -} - // πŸ“Œ Halaman input nilai per kelas - public function inputNilai() -{ - $kelas = Kelas::all(); + $user = auth()->user(); + $mapel_id = $user->mapel_id; - // πŸ”₯ ambil mapel sesuai guru login - $mapel = MataPelajaran::find(auth()->user()->mapel_id); + // Ambil semua nilai siswa di kelas ini untuk mapel guru + $nilaiList = Nilai::with('siswa') + ->where('mapel_id', $mapel_id) + ->whereHas('siswa', fn($q) => $q->where('kelas_id', $request->kelas_id)) + ->get(); - if (!$mapel) { - return back()->with('error', 'Mapel belum disetting!'); + foreach ($nilaiList as $n) { + // Hitung rata-rata dari tugas, uts, uas + $parts = []; + if ($n->nilai_tugas !== null) $parts[] = (float) $n->nilai_tugas; + if ($n->nilai_uts !== null) $parts[] = (float) $n->nilai_uts; + if ($n->nilai_uas !== null) $parts[] = (float) $n->nilai_uas; + + if (count($parts) > 0) { + $rata = round(array_sum($parts) / count($parts), 2); + $n->update(['nilai_akhir' => $rata]); + } + } + + return redirect()->route('guru.nilai.index', ['kelas_id' => $request->kelas_id]) + ->with('success', 'Rata-rata berhasil dihitung!'); } - return view('guru.nilai.input', compact('kelas', 'mapel')); -} + // ── KIRIM NILAI KE SISWA (per kelas atau semua) ───────────────── + public function kirimKeSiswa(Request $request) + { + $user = auth()->user(); + $mapel_id = $user->mapel_id; - // πŸ“Œ Ambil siswa berdasarkan kelas (AJAX) - public function getSiswaByKelas($id) -{ - $siswa = Siswa::where('kelas_id', $id)->get(); + $query = Nilai::where('mapel_id', $mapel_id) + ->whereNotNull('nilai_akhir'); - return response()->json($siswa); -} + // Kalau ada kelas_id, kirim per kelas saja + if ($request->kelas_id) { + $query->whereHas('siswa', fn($q) => $q->where('kelas_id', $request->kelas_id)); + } - // πŸ“Œ Simpan nilai banyak sekaligus + $nilaiList = $query->get(); + + foreach ($nilaiList as $n) { + $n->update(['sudah_kirim' => 1]); + } + + $msg = $request->kelas_id + ? 'Nilai berhasil dikirim ke siswa kelas ini!' + : 'Nilai berhasil dikirim ke semua kelas!'; + + return redirect()->route('guru.nilai.index') + ->with('success', $msg); + } + + // ── HALAMAN INPUT NILAI PER KELAS ──────────────────────────────── + public function inputNilai() + { + $kelas = Kelas::all(); + $mapel = MataPelajaran::find(auth()->user()->mapel_id); + + if (!$mapel) { + return back()->with('error', 'Mapel belum disetting!'); + } + + return view('guru.nilai.input', compact('kelas', 'mapel')); + } + + // ── AJAX: ambil siswa berdasarkan kelas ───────────────────────── + public function getSiswaByKelas($id) + { + $siswa = Siswa::where('kelas_id', $id)->get(); + return response()->json($siswa); + } + + // ── SIMPAN NILAI BATCH ─────────────────────────────────────────── public function storeBatch(Request $request) { $request->validate([ 'mapel_id' => 'required', - 'nilai' => 'required|array' + 'nilai' => 'required|array', ]); foreach ($request->nilai as $siswa_id => $nilai) { - if ($nilai != null) { Nilai::updateOrCreate( - [ - 'siswa_id' => $siswa_id, - 'mapel_id' => $request->mapel_id - ], - [ - 'nilai' => $nilai - ] + ['siswa_id' => $siswa_id, 'mapel_id' => $request->mapel_id], + ['nilai' => $nilai] ); } } return redirect()->route('guru.nilai.index') - ->with('success', 'Nilai berhasil disimpan'); + ->with('success', 'Nilai berhasil disimpan'); } - // ================= CRUD BIASA ================= - - public function create() - { - $siswa = Siswa::all(); - $mapel = MataPelajaran::all(); - - return view('guru.nilai.create', compact('siswa', 'mapel')); - } - - public function store(Request $request) + // ── MASUKKAN NILAI TUGAS DARI MENU TUGAS ───────────────────────── + // Dipanggil dari tombol di halaman tugas / rekap rata-rata + public function masukkanNilaiTugas(Request $request) { $request->validate([ - 'siswa_id' => 'required', - 'mapel_id' => 'required', - 'nilai' => 'required|numeric|min:0|max:100', + 'siswa_id' => 'required', + 'nilai_tugas' => 'required|numeric|min:0|max:100', ]); - Nilai::create($request->all()); + $user = auth()->user(); + $mapel_id = $user->mapel_id; - return redirect()->route('guru.nilai.index') - ->with('success', 'Data nilai berhasil ditambahkan'); + Nilai::updateOrCreate( + ['siswa_id' => $request->siswa_id, 'mapel_id' => $mapel_id], + ['nilai_tugas' => $request->nilai_tugas] + ); + + return back()->with('success', 'Nilai tugas berhasil dimasukkan ke menu nilai!'); } - public function edit($id) -{ - $nilai = Nilai::with(['siswa', 'mapel'])->findOrFail($id); + // ── MASUKKAN NILAI UJIAN DARI MENU UJIAN ───────────────────────── + // Dipanggil dari tombol di menu ujian per ujian (UTS/UAS) + public function masukkanNilaiUjian(Request $request) + { + $request->validate([ + 'ujian_id' => 'required', + ]); - return view('guru.nilai.edit', compact('nilai')); -} + $ujian = \App\Models\Ujian::with('kelas')->findOrFail($request->ujian_id); + $mapel_id = $ujian->mapel_id; + $jenis = strtolower($ujian->jenis); // 'uts' atau 'uas' + $kolom = 'nilai_' . $jenis; // 'nilai_uts' atau 'nilai_uas' + + // Ambil semua hasil ujian ini + $hasilList = \App\Models\Hasil::where('ujian_id', $ujian->id)->get(); + + foreach ($hasilList as $h) { + Nilai::updateOrCreate( + ['siswa_id' => $h->siswa_id, 'mapel_id' => $mapel_id], + [$kolom => $h->nilai] + ); + } + + return back()->with('success', 'Nilai ' . strtoupper($jenis) . ' berhasil dikirim ke menu nilai!'); + } + + // ── EDIT ───────────────────────────────────────────────────────── + public function edit($id) + { + $nilai = Nilai::with(['siswa', 'mapel'])->findOrFail($id); + return view('guru.nilai.edit', compact('nilai')); + } public function update(Request $request, $id) -{ - $request->validate([ - 'nilai' => 'required|numeric|min:0|max:100' - ]); + { + $request->validate([ + 'nilai_tugas' => 'nullable|numeric|min:0|max:100', + 'nilai_uts' => 'nullable|numeric|min:0|max:100', + 'nilai_uas' => 'nullable|numeric|min:0|max:100', + ]); - $nilai = Nilai::findOrFail($id); + $nilai = Nilai::findOrFail($id); + $nilai->update($request->only(['nilai_tugas', 'nilai_uts', 'nilai_uas'])); - $nilai->update([ - 'nilai' => $request->nilai - ]); - - return redirect()->route('guru.nilai.index') - ->with('success', 'Nilai berhasil diupdate'); -} + return redirect()->route('guru.nilai.index') + ->with('success', 'Nilai berhasil diupdate'); + } public function destroy($id) { Nilai::findOrFail($id)->delete(); - return redirect()->route('guru.nilai.index') - ->with('success', 'Data nilai berhasil dihapus'); + ->with('success', 'Data nilai berhasil dihapus'); } } \ No newline at end of file diff --git a/app/Http/Controllers/guru/TugasController.php b/app/Http/Controllers/guru/TugasController.php index 5f17097..1f0ada4 100644 --- a/app/Http/Controllers/guru/TugasController.php +++ b/app/Http/Controllers/guru/TugasController.php @@ -8,91 +8,108 @@ use App\Models\Kelas; use App\Models\MataPelajaran; use App\Models\PengumpulanTugas; -use App\Models\Jadwal; // πŸ”₯ WAJIB +use App\Models\Jadwal; use Illuminate\Support\Facades\Storage; use Carbon\Carbon; use App\Models\Siswa; class TugasController extends Controller { - public function index() -{ - $guru = auth()->user()->guru; + public function index() + { + $guru = auth()->user()->guru; - $tugas = Tugas::with(['kelas', 'mapel']) - ->withCount('pengumpulan') - ->where('guru_id', $guru->id) - ->get(); + $tugas = Tugas::with(['kelas', 'mapel']) + ->withCount('pengumpulan') + ->where('guru_id', $guru->id) + ->get(); - foreach ($tugas as $t) { - $t->total_siswa = Siswa::where('kelas_id', $t->kelas_id)->count(); + foreach ($tugas as $t) { + $t->total_siswa = Siswa::where('kelas_id', $t->kelas_id)->count(); + } + + $tugasIds = $tugas->pluck('id'); + + // ----------------------------------------------------------------- + // Rekap nilai dikelompokkan per KELAS (untuk filter JS di view) + // Format: { "X IPA 1": [ {id, nama, inisial, jumlah_tugas, rata_rata}, … ], … } + // ----------------------------------------------------------------- + $rekapPerKelas = \App\Models\PengumpulanTugas::with(['siswa', 'tugas.kelas']) + ->whereIn('tugas_id', $tugasIds) + ->whereNotNull('nilai') + ->get() + ->groupBy(function ($p) { + // Kelompokkan berdasarkan nama kelas siswa + return $p->tugas->kelas->nama_kelas ?? 'Tidak Diketahui'; + }) + ->map(function ($kelasGroup) { + // Di dalam setiap kelas, kelompokkan lagi per siswa + return $kelasGroup + ->groupBy('siswa_id') + ->map(function ($kumpulan) { + $siswa = $kumpulan->first()->siswa; + $nilaiList = $kumpulan->pluck('nilai')->map(fn ($n) => (float) $n); + $rata = round($nilaiList->avg(), 1); + + return [ + 'id' => $siswa->id ?? 0, + 'nama' => $siswa->nama ?? '-', + 'inisial' => strtoupper(substr($siswa->nama ?? 'S', 0, 1)) + . (isset(explode(' ', $siswa->nama ?? '')[1]) + ? strtoupper(substr(explode(' ', $siswa->nama)[1], 0, 1)) + : ''), + 'jumlah_tugas' => $kumpulan->count(), + 'rata_rata' => $rata, + ]; + }) + ->values(); // reset key jadi array numerik + }); + + // Juga tetap sediakan $rekapNilai (flat) kalau masih dipakai view lain + $rekapNilai = $rekapPerKelas->flatten(1)->values(); + + return view('guru.tugas.index', compact('tugas', 'rekapNilai', 'rekapPerKelas')); } - // Ambil semua pengumpulan dari semua tugas guru ini - $tugasIds = $tugas->pluck('id'); + public function create() + { + $kelas = Kelas::all(); + $guru = auth()->user()->guru; + $mapel = $guru->mapelRel; - $rekapNilai = \App\Models\PengumpulanTugas::with('siswa') - ->whereIn('tugas_id', $tugasIds) - ->whereNotNull('nilai') - ->get() - ->groupBy('siswa_id') - ->map(function($kumpulan) { - $siswa = $kumpulan->first()->siswa; - $nilaiList = $kumpulan->pluck('nilai')->map(fn($n) => (float)$n); - $rata = round($nilaiList->avg(), 1); - return [ - 'nama' => $siswa->nama ?? '-', - 'inisial' => strtoupper(substr($siswa->nama ?? 'S', 0, 1)), - 'jumlah_tugas' => $kumpulan->count(), - 'rata_rata' => $rata, - ]; - }) - ->values(); - - return view('guru.tugas.index', compact('tugas', 'rekapNilai')); -} - -public function create() -{ - $kelas = Kelas::all(); - $guru = auth()->user()->guru; - - $mapel = $guru->mapelRel; - - return view('guru.tugas.create', compact('kelas', 'mapel')); -} - - public function store(Request $request) -{ - $guru = auth()->user()->guru; - - $request->validate([ - 'kelas_id' => 'required', - 'judul' => 'required', - 'deadline' => 'required', - 'file' => 'nullable|mimes:pdf,doc,docx,ppt,pptx|max:2048' - ]); - - $filePath = null; - - // πŸ”₯ INI BAGIAN PALING PENTING - if ($request->hasFile('file')) { - $filePath = $request->file('file')->store('tugas_file', 'public'); + return view('guru.tugas.create', compact('kelas', 'mapel')); } - Tugas::create([ - 'guru_id' => $guru->id, - 'kelas_id' => $request->kelas_id, - 'mapel_id' => $guru->mapel_id, - 'judul' => $request->judul, - 'deskripsi' => $request->deskripsi, - 'file' => $filePath, - 'deadline' => str_replace('T', ' ', $request->deadline), -]); + public function store(Request $request) + { + $guru = auth()->user()->guru; + + $request->validate([ + 'kelas_id' => 'required', + 'judul' => 'required', + 'deadline' => 'required', + 'file' => 'nullable|mimes:pdf,doc,docx,ppt,pptx|max:2048', + ]); + + $filePath = null; + if ($request->hasFile('file')) { + $filePath = $request->file('file')->store('tugas_file', 'public'); + } + + Tugas::create([ + 'guru_id' => $guru->id, + 'kelas_id' => $request->kelas_id, + 'mapel_id' => $guru->mapel_id, + 'judul' => $request->judul, + 'deskripsi' => $request->deskripsi, + 'file' => $filePath, + 'deadline' => str_replace('T', ' ', $request->deadline), + ]); + + return redirect()->route('guru.tugas.index') + ->with('success', 'Tugas berhasil dibuat'); + } - return redirect()->route('guru.tugas.index') - ->with('success', 'Tugas berhasil dibuat'); -} public function getMapelByKelas($id) { $guruId = auth()->user()->guru->id; @@ -106,94 +123,95 @@ public function getMapelByKelas($id) return response()->json($mapel); } + public function show($id) -{ - $tugas = Tugas::with(['kelas', 'mapel'])->findOrFail($id); + { + $tugas = Tugas::with(['kelas', 'mapel'])->findOrFail($id); - return view('guru.tugas.show', compact('tugas')); -} -public function edit($id) -{ - $tugas = Tugas::findOrFail($id); - $kelas = Kelas::all(); - - return view('guru.tugas.edit', compact('tugas', 'kelas')); -} -public function update(Request $request, $id) -{ - $request->validate([ - 'judul' => 'required', - 'kelas_id' => 'required', - 'deadline' => 'required' - ]); - - $tugas = Tugas::findOrFail($id); - - $tugas->update([ - 'judul' => $request->judul, - 'kelas_id' => $request->kelas_id, - 'deskripsi' => $request->deskripsi, - 'deadline' => $request->deadline, - ]); - - return redirect()->route('guru.tugas.index') - ->with('success', 'Tugas berhasil diupdate'); -} - -public function download($id) -{ - $tugas = Tugas::findOrFail($id); - - if (!$tugas->file) { - abort(404, 'File tidak ditemukan'); + return view('guru.tugas.show', compact('tugas')); } - $path = storage_path('app/public/' . $tugas->file); + public function edit($id) + { + $tugas = Tugas::findOrFail($id); + $kelas = Kelas::all(); - if (!file_exists($path)) { - abort(404, 'File tidak ada di storage'); + return view('guru.tugas.edit', compact('tugas', 'kelas')); } - return response()->download($path); -} + public function update(Request $request, $id) + { + $request->validate([ + 'judul' => 'required', + 'kelas_id' => 'required', + 'deadline' => 'required', + ]); -public function pengumpulan($id) -{ - $tugas = Tugas::with(['pengumpulan.siswa'])->findOrFail($id); + $tugas = Tugas::findOrFail($id); - $deadline = Carbon::parse($tugas->deadline); + $tugas->update([ + 'judul' => $request->judul, + 'kelas_id' => $request->kelas_id, + 'deskripsi' => $request->deskripsi, + 'deadline' => $request->deadline, + ]); - foreach ($tugas->pengumpulan as $p) { - $waktu = Carbon::parse($p->created_at); - $p->status = $waktu->gt($deadline) ? 'telat' : 'tepat'; + return redirect()->route('guru.tugas.index') + ->with('success', 'Tugas berhasil diupdate'); } - // Siapkan data untuk JS rata-rata - $pengumpulanJson = $tugas->pengumpulan->map(function($p) { - return [ - 'nama' => $p->siswa->nama ?? '-', - 'inisial' => strtoupper(substr($p->siswa->nama ?? 'S', 0, 1)), - 'nilai' => $p->nilai, - ]; - }); + public function download($id) + { + $tugas = Tugas::findOrFail($id); - return view('guru.tugas.pengumpulan', compact('tugas', 'pengumpulanJson')); -} + if (!$tugas->file) { + abort(404, 'File tidak ditemukan'); + } -public function nilai(Request $request, $id) -{ - $request->validate([ - 'nilai' => 'required|integer|min:0|max:100', - 'komentar' => 'nullable' - ]); + $path = storage_path('app/public/' . $tugas->file); - $pengumpulan = \App\Models\PengumpulanTugas::findOrFail($id); + if (!file_exists($path)) { + abort(404, 'File tidak ada di storage'); + } - $pengumpulan->update([ - 'nilai' => $request->nilai, - 'komentar' => $request->komentar - ]); + return response()->download($path); + } - return back()->with('success', 'Nilai berhasil disimpan'); -} + public function pengumpulan($id) + { + $tugas = Tugas::with(['pengumpulan.siswa'])->findOrFail($id); + $deadline = Carbon::parse($tugas->deadline); + + foreach ($tugas->pengumpulan as $p) { + $waktu = Carbon::parse($p->created_at); + $p->status = $waktu->gt($deadline) ? 'telat' : 'tepat'; + } + + $pengumpulanJson = $tugas->pengumpulan->map(function ($p) { + return [ + 'nama' => $p->siswa->nama ?? '-', + 'inisial' => strtoupper(substr($p->siswa->nama ?? 'S', 0, 1)), + 'nilai' => $p->nilai, + ]; + }); + + return view('guru.tugas.pengumpulan', compact('tugas', 'pengumpulanJson')); + } + + public function nilai(Request $request, $id) + { + $request->validate([ + 'nilai' => 'required|integer|min:0|max:100', + 'komentar' => 'nullable', + ]); + + $pengumpulan = \App\Models\PengumpulanTugas::findOrFail($id); + + $pengumpulan->update([ + 'nilai' => $request->nilai, + 'komentar' => $request->komentar, + ]); + + return back()->with('success', 'Nilai berhasil disimpan'); + } } \ No newline at end of file diff --git a/app/Http/Controllers/siswa/NilaiController.php b/app/Http/Controllers/siswa/NilaiController.php index 98cf22a..70f6c44 100644 --- a/app/Http/Controllers/siswa/NilaiController.php +++ b/app/Http/Controllers/siswa/NilaiController.php @@ -5,90 +5,115 @@ use App\Http\Controllers\Controller; use App\Models\Nilai; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Schema; class NilaiController extends Controller { public function index() -{ - $siswa_id = auth()->user()->siswa_id; + { + $siswa_id = Auth::user()->siswa_id; - // πŸ”Ή Ambil nilai tugas - $tugas = \App\Models\PengumpulanTugas::with('tugas.mapel') - ->where('siswa_id', $siswa_id) - ->get(); + // Cek apakah kolom sudah_kirim sudah ada di tabel nilai + $hasSudahKirim = Schema::hasColumn('nilai', 'sudah_kirim'); - // πŸ”Ή Ambil nilai ujian - $ujian = \App\Models\Hasil::with('ujian.mapel') - ->where('siswa_id', $siswa_id) - ->get(); + $query = Nilai::with('mapel') + ->where('siswa_id', $siswa_id); - // πŸ”₯ REKAP DATA - $data = []; + // Hanya filter sudah_kirim kalau kolomnya sudah ada + if ($hasSudahKirim) { + $query->where('sudah_kirim', 1); + } - // ======================= - // πŸ”Ή PROSES NILAI TUGAS - // ======================= - foreach ($tugas as $t) { - $mapel_id = $t->tugas->mapel->id ?? null; + $nilaiGuru = $query->get(); - if (!$mapel_id) continue; + // Rekap dari nilai guru + $data = []; + foreach ($nilaiGuru as $n) { + $mapel_id = $n->mapel_id; + if (!$mapel_id) continue; + + // Kalau kolom sudah_kirim belum ada, tampilkan yang nilai_akhir-nya sudah terisi saja + if (!$hasSudahKirim && $n->nilai_akhir === null) continue; - if (!isset($data[$mapel_id])) { $data[$mapel_id] = [ - 'mapel' => $t->tugas->mapel->nama_mapel, - 'tugas' => [], - 'uts' => null, - 'uas' => null, + 'mapel' => $n->mapel->nama_mapel ?? 'β€”', + 'tugas' => $n->nilai_tugas !== null ? round($n->nilai_tugas) : null, + 'uts' => $n->nilai_uts !== null ? round($n->nilai_uts) : null, + 'uas' => $n->nilai_uas !== null ? round($n->nilai_uas) : null, + 'rata' => $n->nilai_akhir !== null ? round($n->nilai_akhir) : null, ]; } - $data[$mapel_id]['tugas'][] = $t->nilai; + // Fallback ke sistem lama kalau belum ada data dari guru + if (empty($data)) { + $data = $this->getRekapsLama($siswa_id); + } + + return view('siswa.nilai.index', compact('data')); } - // ======================= - // πŸ”Ή PROSES NILAI UJIAN - // ======================= - foreach ($ujian as $u) { - $mapel_id = $u->ujian->mapel->id ?? null; + /** + * Fallback rekap dari PengumpulanTugas & Hasil (sistem lama) + */ + private function getRekapsLama($siswa_id): array + { + $tugas = \App\Models\PengumpulanTugas::with('tugas.mapel') + ->where('siswa_id', $siswa_id)->get(); - if (!$mapel_id) continue; + $ujian = \App\Models\Hasil::with('ujian.mapel') + ->where('siswa_id', $siswa_id)->get(); - if (!isset($data[$mapel_id])) { - $data[$mapel_id] = [ - 'mapel' => $u->ujian->mapel->nama_mapel, - 'tugas' => [], - 'uts' => null, - 'uas' => null, + $data = []; + + foreach ($tugas as $t) { + $mapel_id = $t->tugas->mapel->id ?? null; + if (!$mapel_id) continue; + if (!isset($data[$mapel_id])) { + $data[$mapel_id] = [ + 'mapel' => $t->tugas->mapel->nama_mapel, + 'tugas_arr' => [], + 'uts' => null, + 'uas' => null, + ]; + } + if ($t->nilai !== null) { + $data[$mapel_id]['tugas_arr'][] = $t->nilai; + } + } + + foreach ($ujian as $u) { + $mapel_id = $u->ujian->mapel->id ?? null; + if (!$mapel_id) continue; + if (!isset($data[$mapel_id])) { + $data[$mapel_id] = [ + 'mapel' => $u->ujian->mapel->nama_mapel, + 'tugas_arr' => [], + 'uts' => null, + 'uas' => null, + ]; + } + if (strtoupper($u->ujian->jenis) === 'UTS') $data[$mapel_id]['uts'] = $u->nilai; + if (strtoupper($u->ujian->jenis) === 'UAS') $data[$mapel_id]['uas'] = $u->nilai; + } + + foreach ($data as $k => $d) { + $rata_tugas = count($d['tugas_arr']) + ? array_sum($d['tugas_arr']) / count($d['tugas_arr']) + : null; + $uts = $d['uts'] ?? null; + $uas = $d['uas'] ?? null; + $parts = array_filter([$rata_tugas, $uts, $uas], fn($v) => $v !== null); + $rata = count($parts) ? round(array_sum($parts) / count($parts)) : null; + + $data[$k] = [ + 'mapel' => $d['mapel'], + 'tugas' => $rata_tugas !== null ? round($rata_tugas) : null, + 'uts' => $uts !== null ? round($uts) : null, + 'uas' => $uas !== null ? round($uas) : null, + 'rata' => $rata, ]; } - if ($u->ujian->jenis == 'UTS') { - $data[$mapel_id]['uts'] = $u->nilai; - } - - if ($u->ujian->jenis == 'UAS') { - $data[$mapel_id]['uas'] = $u->nilai; - } + return $data; } - - // ======================= - // πŸ”Ή HITUNG RATA-RATA - // ======================= - foreach ($data as $k => $d) { - - $rata_tugas = count($d['tugas']) - ? array_sum($d['tugas']) / count($d['tugas']) - : 0; - - $uts = $d['uts'] ?? 0; - $uas = $d['uas'] ?? 0; - - $rata = ($rata_tugas + $uts + $uas) / 3; - - $data[$k]['tugas'] = round($rata_tugas); - $data[$k]['rata'] = round($rata); - } - - return view('siswa.nilai.index', compact('data')); -} } \ No newline at end of file diff --git a/app/Models/Nilai.php b/app/Models/Nilai.php index 773aec9..a852dcb 100644 --- a/app/Models/Nilai.php +++ b/app/Models/Nilai.php @@ -11,14 +11,12 @@ class Nilai extends Model protected $fillable = [ 'siswa_id', 'mapel_id', - - // πŸ”₯ lama (jangan dihapus dulu) - 'nilai', - - // πŸ”₯ baru + 'nilai', // lama, jangan dihapus 'nilai_tugas', 'nilai_uts', - 'nilai_uas' + 'nilai_uas', + 'nilai_akhir', // hasil hitung rata-rata (disimpan permanen) + 'sudah_kirim', // 0 = belum kirim ke siswa, 1 = sudah kirim ]; public function siswa() @@ -31,19 +29,26 @@ public function mapel() return $this->belongsTo(MataPelajaran::class, 'mapel_id'); } - // πŸ”₯ helper biar fleksibel (INI KUNCI AMAN) - public function getNilaiAkhirAttribute() + /** + * Accessor: hitung nilai akhir dinamis (kalau kolom nilai_akhir belum diisi) + */ + public function getNilaiAkhirDinamisAttribute() { - // kalau pakai sistem baru - if ($this->nilai_tugas || $this->nilai_uts || $this->nilai_uas) { - return collect([ - $this->nilai_tugas, - $this->nilai_uts, - $this->nilai_uas - ])->filter()->avg(); + if ($this->nilai_akhir !== null) { + return $this->nilai_akhir; } - // fallback ke sistem lama + $parts = array_filter([ + $this->nilai_tugas !== null ? (float) $this->nilai_tugas : null, + $this->nilai_uts !== null ? (float) $this->nilai_uts : null, + $this->nilai_uas !== null ? (float) $this->nilai_uas : null, + ], fn($v) => $v !== null); + + if (count($parts) > 0) { + return round(array_sum($parts) / count($parts), 2); + } + + // Fallback ke nilai lama return $this->nilai; } } \ No newline at end of file diff --git a/composer.json b/composer.json index cedc908..8980ddc 100644 --- a/composer.json +++ b/composer.json @@ -7,6 +7,7 @@ "license": "MIT", "require": { "php": "^8.2", + "barryvdh/laravel-dompdf": "2.2", "laravel/framework": "^11.31", "laravel/tinker": "^2.9", "simplesoftwareio/simple-qrcode": "^4.2" diff --git a/composer.lock b/composer.lock index 03f90a7..cc03ae0 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ba0b871c4e08ecaa364989ce5e6f5138", + "content-hash": "2135f2e9f15e9229e297b7ea4e12f164", "packages": [ { "name": "bacon/bacon-qr-code", @@ -66,6 +66,89 @@ }, "time": "2022-12-07T17:46:57+00:00" }, + { + "name": "barryvdh/laravel-dompdf", + "version": "v2.2.0", + "source": { + "type": "git", + "url": "https://github.com/barryvdh/laravel-dompdf.git", + "reference": "c96f90c97666cebec154ca1ffb67afed372114d8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/barryvdh/laravel-dompdf/zipball/c96f90c97666cebec154ca1ffb67afed372114d8", + "reference": "c96f90c97666cebec154ca1ffb67afed372114d8", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "dompdf/dompdf": "^2.0.7", + "illuminate/support": "^6|^7|^8|^9|^10|^11", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "larastan/larastan": "^1.0|^2.7.0", + "orchestra/testbench": "^4|^5|^6|^7|^8|^9", + "phpro/grumphp": "^1 || ^2.5", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "PDF": "Barryvdh\\DomPDF\\Facade\\Pdf", + "Pdf": "Barryvdh\\DomPDF\\Facade\\Pdf" + }, + "providers": [ + "Barryvdh\\DomPDF\\ServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Barryvdh\\DomPDF\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Barry vd. Heuvel", + "email": "barryvdh@gmail.com" + } + ], + "description": "A DOMPDF Wrapper for Laravel", + "keywords": [ + "dompdf", + "laravel", + "pdf" + ], + "support": { + "issues": "https://github.com/barryvdh/laravel-dompdf/issues", + "source": "https://github.com/barryvdh/laravel-dompdf/tree/v2.2.0" + }, + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2024-04-25T13:16:04+00:00" + }, { "name": "brick/math", "version": "0.12.3", @@ -493,6 +576,74 @@ ], "time": "2024-02-05T11:56:58+00:00" }, + { + "name": "dompdf/dompdf", + "version": "v2.0.8", + "source": { + "type": "git", + "url": "https://github.com/dompdf/dompdf.git", + "reference": "c20247574601700e1f7c8dab39310fca1964dc52" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dompdf/dompdf/zipball/c20247574601700e1f7c8dab39310fca1964dc52", + "reference": "c20247574601700e1f7c8dab39310fca1964dc52", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "masterminds/html5": "^2.0", + "phenx/php-font-lib": ">=0.5.4 <1.0.0", + "phenx/php-svg-lib": ">=0.5.2 <1.0.0", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "ext-json": "*", + "ext-zip": "*", + "mockery/mockery": "^1.3", + "phpunit/phpunit": "^7.5 || ^8 || ^9", + "squizlabs/php_codesniffer": "^3.5" + }, + "suggest": { + "ext-gd": "Needed to process images", + "ext-gmagick": "Improves image processing performance", + "ext-imagick": "Improves image processing performance", + "ext-zlib": "Needed for pdf stream compression" + }, + "type": "library", + "autoload": { + "psr-4": { + "Dompdf\\": "src/" + }, + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1" + ], + "authors": [ + { + "name": "The Dompdf Community", + "homepage": "https://github.com/dompdf/dompdf/blob/master/AUTHORS.md" + } + ], + "description": "DOMPDF is a CSS 2.1 compliant HTML to PDF converter", + "homepage": "https://github.com/dompdf/dompdf", + "support": { + "issues": "https://github.com/dompdf/dompdf/issues", + "source": "https://github.com/dompdf/dompdf/tree/v2.0.8" + }, + "time": "2024-04-29T13:06:17+00:00" + }, { "name": "dragonmantank/cron-expression", "version": "v3.4.0", @@ -2121,6 +2272,79 @@ ], "time": "2024-12-08T08:18:47+00:00" }, + { + "name": "masterminds/html5", + "version": "2.8.1", + "source": { + "type": "git", + "url": "https://github.com/Masterminds/html5-php.git", + "reference": "f47dcf3c70c584de14f21143c55d9939631bc6cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/f47dcf3c70c584de14f21143c55d9939631bc6cf", + "reference": "f47dcf3c70c584de14f21143c55d9939631bc6cf", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "ext-dom": "*", + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Masterminds\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Matt Butcher", + "email": "technosophos@gmail.com" + }, + { + "name": "Matt Farina", + "email": "matt@mattfarina.com" + }, + { + "name": "Asmir Mustafic", + "email": "goetas@gmail.com" + } + ], + "description": "An HTML5 parser and serializer.", + "homepage": "http://masterminds.github.io/html5-php", + "keywords": [ + "HTML5", + "dom", + "html", + "parser", + "querypath", + "serializer", + "xml" + ], + "support": { + "issues": "https://github.com/Masterminds/html5-php/issues", + "source": "https://github.com/Masterminds/html5-php/tree/2.8.1" + }, + "time": "2023-05-10T11:58:31+00:00" + }, { "name": "monolog/monolog", "version": "3.9.0", @@ -2625,6 +2849,108 @@ ], "time": "2025-05-08T08:14:37+00:00" }, + { + "name": "phenx/php-font-lib", + "version": "0.5.6", + "source": { + "type": "git", + "url": "https://github.com/dompdf/php-font-lib.git", + "reference": "a1681e9793040740a405ac5b189275059e2a9863" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dompdf/php-font-lib/zipball/a1681e9793040740a405ac5b189275059e2a9863", + "reference": "a1681e9793040740a405ac5b189275059e2a9863", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "ext-mbstring": "*" + }, + "require-dev": { + "symfony/phpunit-bridge": "^3 || ^4 || ^5 || ^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "FontLib\\": "src/FontLib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-or-later" + ], + "authors": [ + { + "name": "Fabien MΓ©nager", + "email": "fabien.menager@gmail.com" + } + ], + "description": "A library to read, parse, export and make subsets of different types of font files.", + "homepage": "https://github.com/PhenX/php-font-lib", + "support": { + "issues": "https://github.com/dompdf/php-font-lib/issues", + "source": "https://github.com/dompdf/php-font-lib/tree/0.5.6" + }, + "time": "2024-01-29T14:45:26+00:00" + }, + { + "name": "phenx/php-svg-lib", + "version": "0.5.3", + "source": { + "type": "git", + "url": "https://github.com/dompdf/php-svg-lib.git", + "reference": "0e46722c154726a5f9ac218197ccc28adba16fcf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dompdf/php-svg-lib/zipball/0e46722c154726a5f9ac218197ccc28adba16fcf", + "reference": "0e46722c154726a5f9ac218197ccc28adba16fcf", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "ext-mbstring": "*", + "php": "^7.1 || ^8.0", + "sabberworm/php-css-parser": "^8.4" + }, + "require-dev": { + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Svg\\": "src/Svg" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Fabien MΓ©nager", + "email": "fabien.menager@gmail.com" + } + ], + "description": "A library to read, parse and export to PDF SVG files.", + "homepage": "https://github.com/PhenX/php-svg-lib", + "support": { + "issues": "https://github.com/dompdf/php-svg-lib/issues", + "source": "https://github.com/dompdf/php-svg-lib/tree/0.5.3" + }, + "time": "2024-02-23T20:39:24+00:00" + }, { "name": "phpoption/phpoption", "version": "1.9.3", @@ -3388,6 +3714,78 @@ }, "time": "2025-06-25T14:20:11+00:00" }, + { + "name": "sabberworm/php-css-parser", + "version": "v8.9.0", + "source": { + "type": "git", + "url": "https://github.com/MyIntervals/PHP-CSS-Parser.git", + "reference": "d8e916507b88e389e26d4ab03c904a082aa66bb9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/d8e916507b88e389e26d4ab03c904a082aa66bb9", + "reference": "d8e916507b88e389e26d4ab03c904a082aa66bb9", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "ext-iconv": "*", + "php": "^5.6.20 || ^7.0.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0" + }, + "require-dev": { + "phpunit/phpunit": "5.7.27 || 6.5.14 || 7.5.20 || 8.5.41", + "rawr/cross-data-providers": "^2.0.0" + }, + "suggest": { + "ext-mbstring": "for parsing UTF-8 CSS" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Sabberworm\\CSS\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Raphael Schweikert" + }, + { + "name": "Oliver Klee", + "email": "github@oliverklee.de" + }, + { + "name": "Jake Hotson", + "email": "jake.github@qzdesign.co.uk" + } + ], + "description": "Parser for CSS Files written in PHP", + "homepage": "https://www.sabberworm.com/blog/2010/6/10/php-css-parser", + "keywords": [ + "css", + "parser", + "stylesheet" + ], + "support": { + "issues": "https://github.com/MyIntervals/PHP-CSS-Parser/issues", + "source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v8.9.0" + }, + "time": "2025-07-11T13:20:48+00:00" + }, { "name": "simplesoftwareio/simple-qrcode", "version": "4.2.0", diff --git a/database/migrations/2026_05_09_213247_add_sudah_kirim_to_nilai_table.php b/database/migrations/2026_05_09_213247_add_sudah_kirim_to_nilai_table.php new file mode 100644 index 0000000..dedb9bf --- /dev/null +++ b/database/migrations/2026_05_09_213247_add_sudah_kirim_to_nilai_table.php @@ -0,0 +1,25 @@ +boolean('sudah_kirim')->default(false)->after('nilai_akhir'); + }); +} + +public function down(): void +{ + Schema::table('nilai', function (Blueprint $table) { + $table->dropColumn('sudah_kirim'); + }); +} +}; diff --git a/public/uploads/1778813458.jpg b/public/uploads/1778813458.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e8ddd9d3c34c35f4e72c75f2cad9e058b53db8cd GIT binary patch literal 9277 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 literal 0 HcmV?d00001 diff --git a/public/uploads/1778814685.jpg b/public/uploads/1778814685.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e8ddd9d3c34c35f4e72c75f2cad9e058b53db8cd GIT binary patch literal 9277 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 literal 0 HcmV?d00001 diff --git a/resources/views/guru/nilai/edit.blade.php b/resources/views/guru/nilai/edit.blade.php index 683232d..5e8f3f4 100644 --- a/resources/views/guru/nilai/edit.blade.php +++ b/resources/views/guru/nilai/edit.blade.php @@ -1,173 +1,83 @@ @extends('layouts.app') @section('content') - -
-
+
+
+
+
Edit Nilai Siswa
+

Ubah nilai tugas, UTS, dan UAS

+
+
+ {{-- Info siswa --}} +
+
{{ strtoupper(substr($nilai->siswa->nama ?? 'S', 0, 2)) }}
+
+
{{ $nilai->siswa->nama ?? 'β€”' }}
+
{{ $nilai->siswa->kelas->nama_kelas ?? 'β€”' }} Β· {{ $nilai->mapel->nama_mapel ?? 'β€”' }}
+
+
- - - Kembali ke Daftar Nilai - - - - - {{-- SISWA BANNER --}} -
-
{{ strtoupper(substr($nilai->siswa->nama, 0, 2)) }}
-
-
{{ $nilai->siswa->nama }}
-
{{ $nilai->mapel->nama_mapel }}
-
-
-
Nilai Saat Ini
-
{{ $nilai->nilai }}
-
-
- -
-
-
- Perbarui Nilai -
-
-
+ @csrf @method('PUT') -
- - +
+ +
-
- - + +
+ +
-
- -
- - / 100 -
+ +
+ + +
+ +
-
- -
@endsection \ No newline at end of file diff --git a/resources/views/guru/nilai/index.blade.php b/resources/views/guru/nilai/index.blade.php index 7e304f3..23a6e2d 100644 --- a/resources/views/guru/nilai/index.blade.php +++ b/resources/views/guru/nilai/index.blade.php @@ -3,111 +3,142 @@ @section('content') +{{-- TOAST ALERT (pojok kanan atas, auto-dismiss 4 detik) --}} +@if(session('success')) +
+
+
+ + + +
+ {{ session('success') }} + +
+
+ +@endif +
{{-- HEADER --}} @@ -119,35 +150,78 @@
-

Data Nilai Siswa

-

Kelola dan pantau nilai akademik siswa

+

Menu Nilai

+

Rekap nilai tugas, UTS, UAS & hitung rata-rata siswa

- - - - - Input Nilai + + + Input Nilai Manual
- {{-- TABLE CARD --}} + {{-- INFO --}} +
+ + Nilai tugas & ujian otomatis masuk sini saat diklik "Masukkan ke Menu Nilai" di menu Tugas atau Ujian. Setelah semua terisi, pilih kelas β†’ klik Hitung Rata-Rata β†’ lalu Kirim ke Siswa. +
+ + {{-- FILTER --}} +
+
+ Filter Kelas: + + + @if(request('kelas_id')) + Filter Siswa: + + @endif + + @if(request('kelas_id') || request('siswa_id')) + βœ• Reset + @endif +
+
+ + {{-- TABEL NILAI --}}
- Daftar Nilai{{ $nilai->count() }} data -
-
- Filter: - -
-
+ + Daftar Nilai + {{ $nilai->count() }} data + + @if(request('kelas_id')) +
+ {{-- TOMBOL HITUNG RATA-RATA --}} +
+ @csrf + + +
+ {{-- TOMBOL KIRIM KE KELAS INI --}} +
+ @csrf + + +
+
+ @endif
@@ -157,15 +231,25 @@ No Nama Siswa Kelas - Mata Pelajaran - Nilai - Aksi + Nilai Tugas + Nilai UTS + Nilai UAS + Rata-Rata + Status Kirim + Aksi @forelse($nilai as $n) + @php + $rata = $n->nilai_akhir; + $rataClass = !$rata ? 'chip-null' : ($rata >= 85 ? 'chip-a' : ($rata >= 70 ? 'chip-b' : ($rata >= 55 ? 'chip-c' : 'chip-d'))); + $tugasClass = !$n->nilai_tugas ? 'chip-null' : ($n->nilai_tugas >= 85 ? 'chip-a' : ($n->nilai_tugas >= 70 ? 'chip-b' : ($n->nilai_tugas >= 55 ? 'chip-c' : 'chip-d'))); + $utsClass = !$n->nilai_uts ? 'chip-null' : ($n->nilai_uts >= 85 ? 'chip-a' : ($n->nilai_uts >= 70 ? 'chip-b' : ($n->nilai_uts >= 55 ? 'chip-c' : 'chip-d'))); + $uasClass = !$n->nilai_uas ? 'chip-null' : ($n->nilai_uas >= 85 ? 'chip-a' : ($n->nilai_uas >= 70 ? 'chip-b' : ($n->nilai_uas >= 55 ? 'chip-c' : 'chip-d'))); + @endphp - {{ $loop->iteration }} + {{ $loop->iteration }}
{{ strtoupper(substr($n->siswa->nama ?? 'S', 0, 2)) }}
@@ -173,32 +257,33 @@
{{ $n->siswa->kelas->nama_kelas ?? 'β€”' }} - {{ $n->mapel->nama_mapel ?? 'β€”' }} - - - {{ $n->nilai }} - + {{ $n->nilai_tugas ?? 'β€”' }} + {{ $n->nilai_uts ?? 'β€”' }} + {{ $n->nilai_uas ?? 'β€”' }} + {{ $rata ?? 'β€”' }} + + @if($n->sudah_kirim) + Sudah Dikirim + @else + Belum + @endif - - - - - + + + Edit @empty - +
- - - +
Belum ada data nilai
-
Mulai input nilai dengan klik tombol Input Nilai di atas.
+
Masukkan nilai dari menu Tugas atau Ujian terlebih dahulu.
@@ -207,9 +292,20 @@
+ {{-- FOOTER + TOMBOL KIRIM SEMUA KELAS --}}
diff --git a/resources/views/guru/tugas/index.blade.php b/resources/views/guru/tugas/index.blade.php index f76326c..a445958 100644 --- a/resources/views/guru/tugas/index.blade.php +++ b/resources/views/guru/tugas/index.blade.php @@ -85,91 +85,191 @@ .table-footer { display:flex; align-items:center; justify-content:space-between; padding:13px 22px; border-top:1px solid var(--border); background:var(--surface-secondary); font-size:12.5px; color:var(--text-muted); } -/* RATA-RATA SECTION */ -.btn-hitung-rata { - display: inline-flex; align-items: center; gap: 8px; - padding: 10px 22px; border-radius: var(--radius-md); border: none; - background: var(--success); color: #fff; - font-size: 13.5px; font-weight: 600; - font-family: 'Plus Jakarta Sans', sans-serif; - cursor: pointer; transition: all .18s; - box-shadow: 0 2px 8px rgba(22,163,74,.25); - margin-top: 24px; +/* ===== FILTER RATA-RATA ===== */ +.filter-section { margin-top:24px; } +.filter-card { + background:var(--surface); border:1px solid var(--border); + border-radius:var(--radius-xl); box-shadow:var(--shadow-md); overflow:hidden; } -.btn-hitung-rata:hover { background: #15803d; transform: translateY(-1px); } -.btn-hitung-rata svg { width: 15px; height: 15px; } +.filter-card-header { + display:flex; align-items:center; justify-content:space-between; + padding:16px 22px; border-bottom:1px solid var(--border); + background:var(--surface-secondary); flex-wrap:wrap; gap:12px; +} +.filter-header-left { display:flex; align-items:center; gap:12px; } +.filter-header-icon { + width:38px; height:38px; border-radius:var(--radius-md); + background:var(--primary-light); border:1px solid var(--primary-border); + display:flex; align-items:center; justify-content:center; flex-shrink:0; +} +.filter-header-icon svg { width:18px; height:18px; color:var(--primary); } +.filter-header-title { font-size:14px; font-weight:700; color:var(--text-primary); margin:0; } +.filter-header-sub { font-size:12px; color:var(--text-secondary); margin:2px 0 0; } -.rata-section { margin-top: 20px; animation: fadeUp 0.3s ease both; } +.btn-hitung-rata2 { + display:inline-flex; align-items:center; gap:7px; + padding:10px 20px; border-radius:var(--radius-md); border:none; + background:var(--success); color:#fff; + font-size:13.5px; font-weight:600; font-family:'Plus Jakarta Sans',sans-serif; + cursor:pointer; transition:all .18s; box-shadow:0 2px 8px rgba(22,163,74,.22); +} +.btn-hitung-rata2:hover { background:#15803d; transform:translateY(-1px); } +.btn-hitung-rata2 svg { width:15px; height:15px; } -.rata-card { - background: var(--surface); border: 1px solid var(--border); - border-radius: var(--radius-xl); box-shadow: var(--shadow-md); overflow: hidden; +.filter-body { padding:18px 22px; } +.filter-select-all-row { + display:flex; align-items:center; justify-content:space-between; + margin-bottom:14px; flex-wrap:wrap; gap:8px; } -.rata-card-toolbar { - display: flex; align-items: center; justify-content: space-between; - padding: 16px 22px; border-bottom: 1px solid var(--border); - background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%); +.filter-select-all-row span { font-size:13px; color:var(--text-secondary); font-weight:500; } +.filter-quick-links { display:flex; gap:14px; } +.filter-quick-link { + font-size:12.5px; color:var(--primary); cursor:pointer; + background:none; border:none; font-family:'Plus Jakarta Sans',sans-serif; + padding:0; font-weight:600; } -.rata-toolbar-left { display: flex; align-items: center; gap: 10px; } -.rata-toolbar-icon { - width: 34px; height: 34px; border-radius: 9px; - background: var(--success); color: #fff; - display: flex; align-items: center; justify-content: center; -} -.rata-toolbar-icon svg { width: 16px; height: 16px; } -.rata-toolbar-title { font-size: 13.5px; font-weight: 700; color: #166534; margin: 0; } -.rata-toolbar-sub { font-size: 11.5px; color: #15803d; margin: 2px 0 0; } +.filter-quick-link:hover { text-decoration:underline; } -table.rata-table { width: 100%; border-collapse: collapse; min-width: 600px; } -.rata-table thead tr { - background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%); - border-bottom: 2px solid #bbf7d0; +.kelas-group { margin-bottom:12px; border:1px solid var(--border); border-radius:var(--radius-lg); overflow:hidden; } +.kelas-group:last-child { margin-bottom:0; } +.kelas-group-header { + display:flex; align-items:center; gap:10px; padding:10px 16px; + background:var(--surface-secondary); border-bottom:1px solid var(--border); + cursor:pointer; user-select:none; } -.rata-table thead th { - padding: 12px 16px; font-size: 11px; font-weight: 700; - color: #166534; text-transform: uppercase; letter-spacing: .7px; - white-space: nowrap; +.kelas-group-header:hover { background:#f1f5f9; } +.kelas-group-header input[type=checkbox] { + width:16px; height:16px; accent-color:var(--primary); cursor:pointer; flex-shrink:0; } -.rata-table thead th.center { text-align: center; } -.rata-table tbody tr { border-bottom: 1px solid var(--border); transition: background .15s; } -.rata-table tbody tr:last-child { border-bottom: none; } -.rata-table tbody tr:hover { background: #f0fdf4; } -.rata-table tbody td { padding: 14px 16px; font-size: 13.5px; color: var(--text-primary); vertical-align: middle; } -.rata-table tbody td.center { text-align: center; } +.kelas-label-name { font-size:13.5px; font-weight:700; color:var(--text-primary); flex:1; } +.kelas-count-badge { + background:var(--primary-light); color:var(--primary); + border:1px solid var(--primary-border); border-radius:999px; + font-size:11.5px; font-weight:600; padding:2px 9px; +} +.kelas-avg-preview { + font-size:12px; color:var(--text-muted); font-weight:500; +} +.kelas-toggle-btn { + width:24px; height:24px; border:none; background:none; + cursor:pointer; display:flex; align-items:center; justify-content:center; + color:var(--text-muted); font-size:12px; border-radius:4px; + transition:background 0.15s; flex-shrink:0; +} +.kelas-toggle-btn:hover { background:var(--border); } -.rata-avatar-row { display: flex; align-items: center; gap: 10px; } +.siswa-list-wrapper { padding:0; } +.siswa-item { + display:flex; align-items:center; gap:10px; + padding:10px 16px; border-bottom:1px solid var(--border); cursor:pointer; + transition:background 0.12s; user-select:none; +} +.siswa-item:last-child { border-bottom:none; } +.siswa-item:hover { background:#f8faff; } +.siswa-item input[type=checkbox] { + width:15px; height:15px; accent-color:var(--primary); cursor:pointer; flex-shrink:0; +} +.siswa-avatar { + width:32px; height:32px; border-radius:50%; + background:var(--primary-light); border:1.5px solid var(--primary-border); + display:flex; align-items:center; justify-content:center; + font-size:12px; font-weight:800; color:var(--primary); flex-shrink:0; +} +.siswa-nama-text { font-size:13px; font-weight:600; color:var(--text-primary); flex:1; } +.siswa-tugas-text { font-size:12px; color:var(--text-muted); margin-right:4px; } +.chip-nilai-sm { + display:inline-flex; padding:3px 10px; border-radius:999px; + font-size:11.5px; font-weight:700; +} +.chip-a { background:#dcfce7; color:#166534; } +.chip-b { background:var(--primary-light); color:var(--primary); } +.chip-c { background:var(--warning-light); color:var(--warning); } +.chip-d { background:var(--danger-light); color:var(--danger); } + +/* ===== RESULT TABLE ===== */ +.rata-result-section { margin-top:20px; animation:fadeUp 0.3s ease both; } +.rata-result-card { + background:var(--surface); border:1px solid var(--border); + border-radius:var(--radius-xl); box-shadow:var(--shadow-md); overflow:hidden; +} +.rata-result-header { + padding:16px 22px; border-bottom:1px solid var(--border); + background:linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%); + display:flex; align-items:center; gap:10px; +} +.rata-result-icon { + width:34px; height:34px; border-radius:9px; + background:var(--success); color:#fff; + display:flex; align-items:center; justify-content:center; flex-shrink:0; +} +.rata-result-icon svg { width:16px; height:16px; } +.rata-result-title { font-size:13.5px; font-weight:700; color:#166534; margin:0; } +.rata-result-sub { font-size:11.5px; color:#15803d; margin:2px 0 0; } + +.kelas-result-block { border-bottom:1px solid var(--border); } +.kelas-result-block:last-of-type { border-bottom:none; } +.kelas-result-header { + padding:10px 20px; background:var(--surface-secondary); + display:flex; align-items:center; justify-content:space-between; + border-bottom:1px solid var(--border); +} +.kelas-result-name { + font-size:11px; font-weight:700; color:var(--text-muted); + text-transform:uppercase; letter-spacing:0.6px; +} +.kelas-result-avg { + display:inline-flex; align-items:center; gap:6px; + font-size:12.5px; font-weight:700; +} + +table.rata-result-table { width:100%; border-collapse:collapse; min-width:560px; } +.rata-result-table thead tr { background:linear-gradient(135deg, #f0fdf4, #dcfce7); border-bottom:2px solid #bbf7d0; } +.rata-result-table thead th { + padding:11px 16px; font-size:11px; font-weight:700; + color:#166534; text-transform:uppercase; letter-spacing:.6px; white-space:nowrap; +} +.rata-result-table thead th.c { text-align:center; } +.rata-result-table tbody tr { border-bottom:1px solid var(--border); transition:background .15s; } +.rata-result-table tbody tr:last-child { border-bottom:none; } +.rata-result-table tbody tr:hover { background:#f0fdf4; } +.rata-result-table tbody td { padding:13px 16px; font-size:13.5px; color:var(--text-primary); vertical-align:middle; } +.rata-result-table tbody td.c { text-align:center; } + +.rata-avatar-row { display:flex; align-items:center; gap:10px; } .rata-avatar { - width: 36px; height: 36px; border-radius: 50%; - background: var(--success-light); border: 2px solid #bbf7d0; - display: flex; align-items: center; justify-content: center; - font-size: 13px; font-weight: 800; color: var(--success); flex-shrink: 0; + width:34px; height:34px; border-radius:50%; + background:var(--success-light); border:1.5px solid var(--success-border); + display:flex; align-items:center; justify-content:center; + font-size:12px; font-weight:800; color:var(--success); flex-shrink:0; } -.rata-nama { font-weight: 700; color: var(--text-primary); font-size: 13.5px; } - -.chip-nilai { - display: inline-flex; padding: 5px 16px; - border-radius: 999px; font-size: 14px; font-weight: 800; +.chip-nilai-lg { + display:inline-flex; padding:5px 16px; border-radius:999px; + font-size:14px; font-weight:800; } -.chip-nilai.a { background: #dcfce7; color: #166534; } -.chip-nilai.b { background: var(--primary-light); color: var(--primary); } -.chip-nilai.c { background: var(--warning-light); color: var(--warning); } -.chip-nilai.d { background: var(--danger-light); color: var(--danger); } - .chip-predikat { - display: inline-flex; align-items: center; gap: 4px; - padding: 4px 12px; border-radius: 999px; font-size: 11.5px; font-weight: 700; + display:inline-flex; padding:4px 11px; border-radius:999px; + font-size:11.5px; font-weight:700; } -.chip-predikat.a { background: #dcfce7; color: #166534; } -.chip-predikat.b { background: var(--primary-light); color: var(--primary); } -.chip-predikat.c { background: var(--warning-light); color: var(--warning); } -.chip-predikat.d { background: var(--danger-light); color: var(--danger); } -.rata-footer { - display: flex; align-items: center; justify-content: space-between; - padding: 13px 22px; border-top: 1px solid var(--border); - background: var(--surface-secondary); font-size: 13px; color: var(--text-muted); +.rata-result-footer { + display:flex; align-items:center; justify-content:space-between; + padding:13px 22px; border-top:1px solid var(--border); + background:var(--surface-secondary); font-size:13px; color:var(--text-muted); } -.rata-footer-nilai { font-size: 16px; font-weight: 800; color: var(--text-primary); } +.rata-result-footer-val { font-size:16px; font-weight:800; color:var(--text-primary); } +.rata-empty { padding:40px 24px; text-align:center; font-size:13px; color:var(--text-muted); } + +/* ===== TOMBOL MASUKKAN KE MENU NILAI (baru) ===== */ +.btn-masuk-nilai { + display:inline-flex; align-items:center; gap:5px; + padding:5px 12px; font-size:11.5px; font-weight:700; + font-family:'Plus Jakarta Sans',sans-serif; + background:var(--primary-light); color:var(--primary); + border:1px solid var(--primary-border); + border-radius:6px; cursor:pointer; transition:all .15s; + white-space:nowrap; +} +.btn-masuk-nilai:hover { background:#dbeafe; transform:translateY(-1px); }
@@ -192,6 +292,7 @@
+ {{-- TABEL DAFTAR TUGAS --}}
Daftar Tugas{{ $tugas->count() }} tugas @@ -279,103 +380,330 @@ @endif
- {{-- TOMBOL HITUNG --}} -
- -
- -{{-- TABEL RATA-RATA --}} -