final project

bismillah lancar SIDANG
This commit is contained in:
Ryfandii 2026-05-19 12:08:41 +07:00
parent 3bdc385a6d
commit 9762288c38
17 changed files with 1838 additions and 1216 deletions

View File

@ -0,0 +1,42 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Nilai;
use Barryvdh\DomPDF\Facade\Pdf;
class LaporanAkademikController extends Controller
{
public function index()
{
$nilai = Nilai::with(['siswa', 'mapel'])
->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');
}
}

View File

@ -1,6 +1,6 @@
<?php <?php
namespace App\Http\Controllers\guru; namespace App\Http\Controllers\Guru;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -11,134 +11,213 @@
class NilaiController extends Controller class NilaiController extends Controller
{ {
// 📌 Tampilkan semua nilai // ── INDEX: Tampilkan menu nilai guru ──────────────────────────────
public function index(Request $request) public function index(Request $request)
{ {
$user = auth()->user(); $user = auth()->user();
$mapel_id = $user->mapel_id;
$kelas = Kelas::all();
// ambil mapel guru // Filter kelas
$mapel_id = $user->mapel_id; $query = Nilai::with(['siswa.kelas', 'mapel'])
->where('mapel_id', $mapel_id);
// ambil semua kelas (buat dropdown filter) if ($request->kelas_id) {
$kelas = \App\Models\Kelas::all(); $query->whereHas('siswa', function ($q) use ($request) {
$q->where('kelas_id', $request->kelas_id);
});
}
// query nilai + relasi siswa & mapel // Filter siswa (opsional)
$query = \App\Models\Nilai::with(['siswa.kelas', 'mapel']) if ($request->siswa_id) {
->where('mapel_id', $mapel_id); $query->where('siswa_id', $request->siswa_id);
}
// 🔥 FILTER KELAS $nilai = $query->get();
if ($request->kelas_id) {
$query->whereHas('siswa', function ($q) use ($request) { // Siswa berdasarkan kelas yang dipilih (untuk dropdown filter siswa)
$q->where('kelas_id', $request->kelas_id); $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')); $user = auth()->user();
} $mapel_id = $user->mapel_id;
// 📌 Halaman input nilai per kelas
public function inputNilai()
{
$kelas = Kelas::all();
// 🔥 ambil mapel sesuai guru login // Ambil semua nilai siswa di kelas ini untuk mapel guru
$mapel = MataPelajaran::find(auth()->user()->mapel_id); $nilaiList = Nilai::with('siswa')
->where('mapel_id', $mapel_id)
->whereHas('siswa', fn($q) => $q->where('kelas_id', $request->kelas_id))
->get();
if (!$mapel) { foreach ($nilaiList as $n) {
return back()->with('error', 'Mapel belum disetting!'); // 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) $query = Nilai::where('mapel_id', $mapel_id)
public function getSiswaByKelas($id) ->whereNotNull('nilai_akhir');
{
$siswa = Siswa::where('kelas_id', $id)->get();
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) public function storeBatch(Request $request)
{ {
$request->validate([ $request->validate([
'mapel_id' => 'required', 'mapel_id' => 'required',
'nilai' => 'required|array' 'nilai' => 'required|array',
]); ]);
foreach ($request->nilai as $siswa_id => $nilai) { foreach ($request->nilai as $siswa_id => $nilai) {
if ($nilai != null) { if ($nilai != null) {
Nilai::updateOrCreate( Nilai::updateOrCreate(
[ ['siswa_id' => $siswa_id, 'mapel_id' => $request->mapel_id],
'siswa_id' => $siswa_id, ['nilai' => $nilai]
'mapel_id' => $request->mapel_id
],
[
'nilai' => $nilai
]
); );
} }
} }
return redirect()->route('guru.nilai.index') return redirect()->route('guru.nilai.index')
->with('success', 'Nilai berhasil disimpan'); ->with('success', 'Nilai berhasil disimpan');
} }
// ================= CRUD BIASA ================= // ── MASUKKAN NILAI TUGAS DARI MENU TUGAS ─────────────────────────
// Dipanggil dari tombol di halaman tugas / rekap rata-rata
public function create() public function masukkanNilaiTugas(Request $request)
{
$siswa = Siswa::all();
$mapel = MataPelajaran::all();
return view('guru.nilai.create', compact('siswa', 'mapel'));
}
public function store(Request $request)
{ {
$request->validate([ $request->validate([
'siswa_id' => 'required', 'siswa_id' => 'required',
'mapel_id' => 'required', 'nilai_tugas' => 'required|numeric|min:0|max:100',
'nilai' => 'required|numeric|min:0|max:100',
]); ]);
Nilai::create($request->all()); $user = auth()->user();
$mapel_id = $user->mapel_id;
return redirect()->route('guru.nilai.index') Nilai::updateOrCreate(
->with('success', 'Data nilai berhasil ditambahkan'); ['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) // ── MASUKKAN NILAI UJIAN DARI MENU UJIAN ─────────────────────────
{ // Dipanggil dari tombol di menu ujian per ujian (UTS/UAS)
$nilai = Nilai::with(['siswa', 'mapel'])->findOrFail($id); 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) public function update(Request $request, $id)
{ {
$request->validate([ $request->validate([
'nilai' => 'required|numeric|min:0|max:100' '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([ return redirect()->route('guru.nilai.index')
'nilai' => $request->nilai ->with('success', 'Nilai berhasil diupdate');
]); }
return redirect()->route('guru.nilai.index')
->with('success', 'Nilai berhasil diupdate');
}
public function destroy($id) public function destroy($id)
{ {
Nilai::findOrFail($id)->delete(); Nilai::findOrFail($id)->delete();
return redirect()->route('guru.nilai.index') return redirect()->route('guru.nilai.index')
->with('success', 'Data nilai berhasil dihapus'); ->with('success', 'Data nilai berhasil dihapus');
} }
} }

View File

@ -8,91 +8,108 @@
use App\Models\Kelas; use App\Models\Kelas;
use App\Models\MataPelajaran; use App\Models\MataPelajaran;
use App\Models\PengumpulanTugas; use App\Models\PengumpulanTugas;
use App\Models\Jadwal; // 🔥 WAJIB use App\Models\Jadwal;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Carbon\Carbon; use Carbon\Carbon;
use App\Models\Siswa; use App\Models\Siswa;
class TugasController extends Controller class TugasController extends Controller
{ {
public function index() public function index()
{ {
$guru = auth()->user()->guru; $guru = auth()->user()->guru;
$tugas = Tugas::with(['kelas', 'mapel']) $tugas = Tugas::with(['kelas', 'mapel'])
->withCount('pengumpulan') ->withCount('pengumpulan')
->where('guru_id', $guru->id) ->where('guru_id', $guru->id)
->get(); ->get();
foreach ($tugas as $t) { foreach ($tugas as $t) {
$t->total_siswa = Siswa::where('kelas_id', $t->kelas_id)->count(); $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 public function create()
$tugasIds = $tugas->pluck('id'); {
$kelas = Kelas::all();
$guru = auth()->user()->guru;
$mapel = $guru->mapelRel;
$rekapNilai = \App\Models\PengumpulanTugas::with('siswa') return view('guru.tugas.create', compact('kelas', 'mapel'));
->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');
} }
Tugas::create([ public function store(Request $request)
'guru_id' => $guru->id, {
'kelas_id' => $request->kelas_id, $guru = auth()->user()->guru;
'mapel_id' => $guru->mapel_id,
'judul' => $request->judul, $request->validate([
'deskripsi' => $request->deskripsi, 'kelas_id' => 'required',
'file' => $filePath, 'judul' => 'required',
'deadline' => str_replace('T', ' ', $request->deadline), '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) public function getMapelByKelas($id)
{ {
$guruId = auth()->user()->guru->id; $guruId = auth()->user()->guru->id;
@ -106,94 +123,95 @@ public function getMapelByKelas($id)
return response()->json($mapel); return response()->json($mapel);
} }
public function show($id) 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')); 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');
} }
$path = storage_path('app/public/' . $tugas->file); public function edit($id)
{
$tugas = Tugas::findOrFail($id);
$kelas = Kelas::all();
if (!file_exists($path)) { return view('guru.tugas.edit', compact('tugas', 'kelas'));
abort(404, 'File tidak ada di storage');
} }
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::findOrFail($id);
{
$tugas = Tugas::with(['pengumpulan.siswa'])->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) { return redirect()->route('guru.tugas.index')
$waktu = Carbon::parse($p->created_at); ->with('success', 'Tugas berhasil diupdate');
$p->status = $waktu->gt($deadline) ? 'telat' : 'tepat';
} }
// Siapkan data untuk JS rata-rata public function download($id)
$pengumpulanJson = $tugas->pengumpulan->map(function($p) { {
return [ $tugas = Tugas::findOrFail($id);
'nama' => $p->siswa->nama ?? '-',
'inisial' => strtoupper(substr($p->siswa->nama ?? 'S', 0, 1)),
'nilai' => $p->nilai,
];
});
return view('guru.tugas.pengumpulan', compact('tugas', 'pengumpulanJson')); if (!$tugas->file) {
} abort(404, 'File tidak ditemukan');
}
public function nilai(Request $request, $id)
{ $path = storage_path('app/public/' . $tugas->file);
$request->validate([
'nilai' => 'required|integer|min:0|max:100', if (!file_exists($path)) {
'komentar' => 'nullable' abort(404, 'File tidak ada di storage');
]); }
$pengumpulan = \App\Models\PengumpulanTugas::findOrFail($id); return response()->download($path);
}
$pengumpulan->update([
'nilai' => $request->nilai, public function pengumpulan($id)
'komentar' => $request->komentar {
]); $tugas = Tugas::with(['pengumpulan.siswa'])->findOrFail($id);
$deadline = Carbon::parse($tugas->deadline);
return back()->with('success', 'Nilai berhasil disimpan');
} 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');
}
} }

View File

@ -5,90 +5,115 @@
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Nilai; use App\Models\Nilai;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Schema;
class NilaiController extends Controller class NilaiController extends Controller
{ {
public function index() public function index()
{ {
$siswa_id = auth()->user()->siswa_id; $siswa_id = Auth::user()->siswa_id;
// 🔹 Ambil nilai tugas // Cek apakah kolom sudah_kirim sudah ada di tabel nilai
$tugas = \App\Models\PengumpulanTugas::with('tugas.mapel') $hasSudahKirim = Schema::hasColumn('nilai', 'sudah_kirim');
->where('siswa_id', $siswa_id)
->get();
// 🔹 Ambil nilai ujian $query = Nilai::with('mapel')
$ujian = \App\Models\Hasil::with('ujian.mapel') ->where('siswa_id', $siswa_id);
->where('siswa_id', $siswa_id)
->get();
// 🔥 REKAP DATA // Hanya filter sudah_kirim kalau kolomnya sudah ada
$data = []; if ($hasSudahKirim) {
$query->where('sudah_kirim', 1);
}
// ======================= $nilaiGuru = $query->get();
// 🔹 PROSES NILAI TUGAS
// =======================
foreach ($tugas as $t) {
$mapel_id = $t->tugas->mapel->id ?? null;
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] = [ $data[$mapel_id] = [
'mapel' => $t->tugas->mapel->nama_mapel, 'mapel' => $n->mapel->nama_mapel ?? '—',
'tugas' => [], 'tugas' => $n->nilai_tugas !== null ? round($n->nilai_tugas) : null,
'uts' => null, 'uts' => $n->nilai_uts !== null ? round($n->nilai_uts) : null,
'uas' => 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 * Fallback rekap dari PengumpulanTugas & Hasil (sistem lama)
// ======================= */
foreach ($ujian as $u) { private function getRekapsLama($siswa_id): array
$mapel_id = $u->ujian->mapel->id ?? null; {
$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 = [];
$data[$mapel_id] = [
'mapel' => $u->ujian->mapel->nama_mapel, foreach ($tugas as $t) {
'tugas' => [], $mapel_id = $t->tugas->mapel->id ?? null;
'uts' => null, if (!$mapel_id) continue;
'uas' => null, 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') { return $data;
$data[$mapel_id]['uts'] = $u->nilai;
}
if ($u->ujian->jenis == 'UAS') {
$data[$mapel_id]['uas'] = $u->nilai;
}
} }
// =======================
// 🔹 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'));
}
} }

View File

@ -11,14 +11,12 @@ class Nilai extends Model
protected $fillable = [ protected $fillable = [
'siswa_id', 'siswa_id',
'mapel_id', 'mapel_id',
'nilai', // lama, jangan dihapus
// 🔥 lama (jangan dihapus dulu)
'nilai',
// 🔥 baru
'nilai_tugas', 'nilai_tugas',
'nilai_uts', '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() public function siswa()
@ -31,19 +29,26 @@ public function mapel()
return $this->belongsTo(MataPelajaran::class, 'mapel_id'); 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_akhir !== null) {
if ($this->nilai_tugas || $this->nilai_uts || $this->nilai_uas) { return $this->nilai_akhir;
return collect([
$this->nilai_tugas,
$this->nilai_uts,
$this->nilai_uas
])->filter()->avg();
} }
// 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; return $this->nilai;
} }
} }

View File

@ -7,6 +7,7 @@
"license": "MIT", "license": "MIT",
"require": { "require": {
"php": "^8.2", "php": "^8.2",
"barryvdh/laravel-dompdf": "2.2",
"laravel/framework": "^11.31", "laravel/framework": "^11.31",
"laravel/tinker": "^2.9", "laravel/tinker": "^2.9",
"simplesoftwareio/simple-qrcode": "^4.2" "simplesoftwareio/simple-qrcode": "^4.2"

400
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "ba0b871c4e08ecaa364989ce5e6f5138", "content-hash": "2135f2e9f15e9229e297b7ea4e12f164",
"packages": [ "packages": [
{ {
"name": "bacon/bacon-qr-code", "name": "bacon/bacon-qr-code",
@ -66,6 +66,89 @@
}, },
"time": "2022-12-07T17:46:57+00:00" "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", "name": "brick/math",
"version": "0.12.3", "version": "0.12.3",
@ -493,6 +576,74 @@
], ],
"time": "2024-02-05T11:56:58+00:00" "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", "name": "dragonmantank/cron-expression",
"version": "v3.4.0", "version": "v3.4.0",
@ -2121,6 +2272,79 @@
], ],
"time": "2024-12-08T08:18:47+00:00" "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", "name": "monolog/monolog",
"version": "3.9.0", "version": "3.9.0",
@ -2625,6 +2849,108 @@
], ],
"time": "2025-05-08T08:14:37+00:00" "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", "name": "phpoption/phpoption",
"version": "1.9.3", "version": "1.9.3",
@ -3388,6 +3714,78 @@
}, },
"time": "2025-06-25T14:20:11+00:00" "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", "name": "simplesoftwareio/simple-qrcode",
"version": "4.2.0", "version": "4.2.0",

View File

@ -0,0 +1,25 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('nilai', function (Blueprint $table) {
$table->boolean('sudah_kirim')->default(false)->after('nilai_akhir');
});
}
public function down(): void
{
Schema::table('nilai', function (Blueprint $table) {
$table->dropColumn('sudah_kirim');
});
}
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

@ -1,173 +1,83 @@
@extends('layouts.app') @extends('layouts.app')
@section('content') @section('content')
<style> <style>
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap');
:root{--primary:#2563eb;--primary-light:#eff6ff;--primary-border:#bfdbfe;--primary-dark:#1d4ed8;--success:#16a34a;--text-primary:#0f172a;--text-secondary:#475569;--surface:#ffffff;--surface-secondary:#f8fafc;--border:#e2e8f0;--shadow-md:0 4px 20px rgba(0,0,0,0.08);--radius-md:10px;--radius-lg:14px;--radius-xl:18px;}
:root { *{font-family:'Plus Jakarta Sans',sans-serif;box-sizing:border-box;}
--primary:#2563eb; --primary-light:#eff6ff; --primary-border:#bfdbfe; --primary-dark:#1d4ed8; .page-wrapper{animation:fadeUp 0.4s ease both;}
--success:#16a34a; --success-light:#f0fdf4; --success-border:#bbf7d0; @keyframes fadeUp{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}
--warning:#d97706; --warning-light:#fffbeb; --warning-border:#fde68a; .section-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-xl);box-shadow:var(--shadow-md);overflow:hidden;max-width:540px;margin:0 auto;}
--danger:#dc2626; .card-header{padding:20px 24px;border-bottom:1px solid var(--border);background:var(--surface-secondary);}
--text-primary:#0f172a; --text-secondary:#475569; --text-muted:#94a3b8; .card-title{font-size:16px;font-weight:700;color:var(--text-primary);margin:0 0 2px;}
--surface:#ffffff; --surface-secondary:#f8fafc; --border:#e2e8f0; .card-sub{font-size:13px;color:var(--text-secondary);margin:0;}
--shadow-md:0 4px 20px rgba(0,0,0,0.08), 0 2px 6px rgba(0,0,0,0.04); .card-body{padding:24px;}
--shadow-focus:0 0 0 3px rgba(37,99,235,0.12); .form-group{margin-bottom:18px;}
--radius-sm:6px; --radius-md:10px; --radius-lg:14px; --radius-xl:18px; .form-label{display:block;font-size:13px;font-weight:600;color:var(--text-primary);margin-bottom:6px;}
} .form-label span{font-size:11.5px;font-weight:400;color:var(--text-secondary);margin-left:6px;}
*{ .form-control{width:100%;padding:10px 14px;font-size:13.5px;font-family:'Plus Jakarta Sans',sans-serif;border:1px solid var(--border);border-radius:var(--radius-md);color:var(--text-primary);outline:none;transition:border-color .2s;}
box-sizing:border-box; .form-control:focus{border-color:var(--primary);}
} .form-footer{display:flex;gap:10px;align-items:center;padding-top:4px;}
.btn-save{display:inline-flex;align-items:center;gap:7px;padding:10px 22px;font-size:13.5px;font-weight:600;font-family:'Plus Jakarta Sans',sans-serif;background:var(--primary);color:#fff;border:none;border-radius:var(--radius-md);cursor:pointer;transition:all .15s;box-shadow:0 2px 8px rgba(37,99,235,0.25);}
body, .btn-save:hover{background:var(--primary-dark);transform:translateY(-1px);}
button, .btn-back{display:inline-flex;align-items:center;gap:6px;padding:10px 18px;font-size:13px;font-weight:600;font-family:'Plus Jakarta Sans',sans-serif;background:var(--surface-secondary);color:var(--text-secondary);border:1px solid var(--border);border-radius:var(--radius-md);text-decoration:none;transition:all .15s;}
input, .btn-back:hover{background:var(--border);color:var(--text-primary);text-decoration:none;}
select, .siswa-info{display:flex;align-items:center;gap:12px;background:var(--primary-light);border:1px solid var(--primary-border);border-radius:var(--radius-lg);padding:12px 16px;margin-bottom:22px;}
textarea{ .siswa-avatar{width:40px;height:40px;border-radius:50%;background:var(--primary);color:#fff;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:700;flex-shrink:0;}
font-family:'Plus Jakarta Sans',sans-serif; .siswa-name{font-size:14px;font-weight:700;color:var(--text-primary);}
} .siswa-kelas{font-size:12px;color:var(--text-secondary);margin-top:2px;}
.form-wrapper{
width:100%;
max-width:100%;
animation:fadeUp .4s ease both;
}
@keyframes fadeUp { from{opacity:0;transform:translateY(14px)} to{opacity:1;transform:translateY(0)} }
.back-nav { display:inline-flex; align-items:center; gap:6px; color:var(--text-muted); font-size:13.5px; font-weight:500; text-decoration:none; margin-bottom:22px; transition:color 0.15s; }
.back-nav:hover { color:var(--primary); text-decoration:none; }
.back-nav svg { width:16px; height:16px; }
.page-header { display:flex; align-items:center; gap:14px; margin-bottom:24px; }
.page-icon { width:50px; height:50px; border-radius:var(--radius-lg); background:var(--success-light); border:1px solid var(--success-border); display:flex; align-items:center; justify-content:center; flex-shrink:0; }
.page-icon svg { width:24px; height:24px; color:var(--success); }
.page-title { font-size:22px; font-weight:700; color:var(--text-primary); margin:0 0 2px; letter-spacing:-0.3px; }
.page-subtitle { font-size:13px; color:var(--text-secondary); margin:0; }
/* INFO BANNER */
.siswa-banner { display:flex; align-items:center; gap:14px; background:var(--surface-secondary); border:1px solid var(--border); border-radius:var(--radius-lg); padding:16px 20px; margin-bottom:22px; }
.siswa-avatar { width:44px; height:44px; border-radius:var(--radius-md); background:var(--primary-light); border:1px solid var(--primary-border); display:flex; align-items:center; justify-content:center; font-size:15px; font-weight:700; color:var(--primary); flex-shrink:0; }
.siswa-name { font-size:14px; font-weight:600; color:var(--text-primary); }
.siswa-meta { font-size:12.5px; color:var(--text-secondary); margin-top:2px; }
/* CARD */
.form-card { background:var(--surface); border:1px solid var(--border); border-radius:var(--radius-xl); box-shadow:var(--shadow-md); overflow:hidden; }
.card-head { padding:16px 22px; border-bottom:1px solid var(--border); background:var(--surface-secondary); display:flex; align-items:center; gap:9px; }
.card-head-dot { width:8px; height:8px; border-radius:50%; background:var(--success); }
.card-head-label { font-size:12.5px; font-weight:700; color:var(--text-secondary); text-transform:uppercase; letter-spacing:0.7px; }
.card-body-p { padding:26px 24px; }
.form-label { display:block; font-size:13px; font-weight:600; color:var(--text-primary); margin-bottom:7px; }
.form-control {
width:100%; padding:10px 14px; font-size:14px; font-family:'Plus Jakarta Sans',sans-serif;
color:var(--text-primary); background:var(--surface); border:1px solid var(--border);
border-radius:var(--radius-md); outline:none; transition:border-color 0.2s, box-shadow 0.2s;
}
.form-control:focus { border-color:var(--primary); box-shadow:var(--shadow-focus); }
.form-control[readonly] { background:var(--surface-secondary); color:var(--text-secondary); }
.mb-3 { margin-bottom:18px; }
/* NILAI INPUT SPECIAL */
.nilai-input-wrap { position:relative; }
.nilai-input-wrap input { font-size:18px; font-weight:700; padding:12px 50px 12px 16px; text-align:center; }
.nilai-unit { position:absolute; right:14px; top:50%; transform:translateY(-50%); font-size:13px; color:var(--text-muted); font-weight:600; pointer-events:none; }
/* CARD FOOTER */
.card-footer-p { display:flex; align-items:center; justify-content:space-between; padding:18px 24px; border-top:1px solid var(--border); background:var(--surface-secondary); flex-wrap:wrap; gap:10px; }
.btn-cancel { display:inline-flex; align-items:center; gap:7px; padding:10px 20px; font-size:13.5px; font-weight:600; font-family:'Plus Jakarta Sans',sans-serif; background:var(--surface); color:var(--text-secondary); border:1px solid var(--border); border-radius:var(--radius-md); text-decoration:none; cursor:pointer; transition:all 0.15s; }
.btn-cancel:hover { background:var(--surface-secondary); color:var(--text-primary); text-decoration:none; }
.btn-cancel svg { width:15px; height:15px; }
.btn-save { display:inline-flex; align-items:center; gap:8px; padding:10px 26px; font-size:13.5px; font-weight:600; font-family:'Plus Jakarta Sans',sans-serif; background:var(--success); color:#fff; border:none; border-radius:var(--radius-md); cursor:pointer; transition:all 0.15s; box-shadow:0 2px 8px rgba(22,163,74,0.3); }
.btn-save:hover { background:#15803d; transform:translateY(-1px); }
.btn-save svg { width:15px; height:15px; }
.edit-page{
width:100%;
padding:24px 28px 40px;
}
.form-grid{
display:grid;
grid-template-columns:repeat(auto-fit,minmax(320px,1fr));
gap:20px;
}
</style> </style>
<div class="edit-page"> <div class="container-fluid px-4 py-3 page-wrapper">
<div class="form-wrapper"> <div class="section-card">
<div class="card-header">
<h5 class="card-title">Edit Nilai Siswa</h5>
<p class="card-sub">Ubah nilai tugas, UTS, dan UAS</p>
</div>
<div class="card-body">
{{-- Info siswa --}}
<div class="siswa-info">
<div class="siswa-avatar">{{ strtoupper(substr($nilai->siswa->nama ?? 'S', 0, 2)) }}</div>
<div>
<div class="siswa-name">{{ $nilai->siswa->nama ?? '—' }}</div>
<div class="siswa-kelas">{{ $nilai->siswa->kelas->nama_kelas ?? '—' }} · {{ $nilai->mapel->nama_mapel ?? '—' }}</div>
</div>
</div>
<a href="{{ route('guru.nilai.index') }}" class="back-nav"> <form action="{{ route('guru.nilai.update', $nilai->id) }}" method="POST">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7"/></svg>
Kembali ke Daftar Nilai
</a>
<div class="page-header">
<div class="page-icon">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8">
<path stroke-linecap="round" stroke-linejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
</svg>
</div>
<div>
<h2 class="page-title">Edit Nilai</h2>
<p class="page-subtitle">Perbarui nilai akademik siswa</p>
</div>
</div>
{{-- SISWA BANNER --}}
<div class="siswa-banner">
<div class="siswa-avatar">{{ strtoupper(substr($nilai->siswa->nama, 0, 2)) }}</div>
<div>
<div class="siswa-name">{{ $nilai->siswa->nama }}</div>
<div class="siswa-meta">{{ $nilai->mapel->nama_mapel }}</div>
</div>
<div style="margin-left:auto; text-align:right;">
<div style="font-size:11px;color:var(--text-muted);font-weight:600;text-transform:uppercase;letter-spacing:0.5px;">Nilai Saat Ini</div>
<div style="font-size:24px;font-weight:700;color:var(--primary);">{{ $nilai->nilai }}</div>
</div>
</div>
<div class="form-card">
<div class="card-head">
<div class="card-head-dot"></div>
<span class="card-head-label">Perbarui Nilai</span>
</div>
<div class="card-body-p">
<form action="{{ route('guru.nilai.update', $nilai->id) }}" method="POST" id="formEditNilai">
@csrf @csrf
@method('PUT') @method('PUT')
<div class="mb-3"> <div class="form-group">
<label class="form-label">Nama Siswa</label> <label class="form-label">Nilai Tugas <span>(rata-rata semua tugas)</span></label>
<input type="text" class="form-control" value="{{ $nilai->siswa->nama }}" readonly> <input type="number" name="nilai_tugas" class="form-control"
value="{{ old('nilai_tugas', $nilai->nilai_tugas) }}"
min="0" max="100" step="0.01" placeholder="0 100">
</div> </div>
<div class="mb-3">
<label class="form-label">Mata Pelajaran</label> <div class="form-group">
<input type="text" class="form-control" value="{{ $nilai->mapel->nama_mapel }}" readonly> <label class="form-label">Nilai UTS</label>
<input type="number" name="nilai_uts" class="form-control"
value="{{ old('nilai_uts', $nilai->nilai_uts) }}"
min="0" max="100" step="0.01" placeholder="0 100">
</div> </div>
<div class="mb-3">
<label class="form-label">Nilai Baru <span style="color:var(--danger);">*</span></label> <div class="form-group">
<div class="nilai-input-wrap"> <label class="form-label">Nilai UAS</label>
<input type="number" name="nilai" class="form-control" value="{{ $nilai->nilai }}" min="0" max="100" required> <input type="number" name="nilai_uas" class="form-control"
<span class="nilai-unit">/ 100</span> value="{{ old('nilai_uas', $nilai->nilai_uas) }}"
</div> min="0" max="100" step="0.01" placeholder="0 100">
</div>
<div class="form-footer">
<button type="submit" class="btn-save">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" style="width:14px;height:14px;"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
Simpan Perubahan
</button>
<a href="{{ route('guru.nilai.index') }}" class="btn-back">Batal</a>
</div> </div>
</form> </form>
</div> </div>
<div class="card-footer-p">
<a href="{{ route('guru.nilai.index') }}" class="btn-cancel">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>
Batalkan
</a>
<button type="submit" form="formEditNilai" class="btn-save">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
Simpan Nilai
</button>
</div>
</div> </div>
</div>
</div> </div>
@endsection @endsection

View File

@ -3,111 +3,142 @@
@section('content') @section('content')
<style> <style>
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap');
:root { :root {
--primary:#2563eb; --primary-light:#eff6ff; --primary-border:#bfdbfe; --primary-dark:#1d4ed8; --primary:#2563eb; --primary-light:#eff6ff; --primary-border:#bfdbfe; --primary-dark:#1d4ed8;
--success:#16a34a; --success-light:#f0fdf4; --success-border:#bbf7d0; --success:#16a34a; --success-light:#f0fdf4; --success-border:#bbf7d0;
--warning:#d97706; --warning-light:#fffbeb; --warning-border:#fde68a; --warning:#d97706; --warning-light:#fffbeb; --warning-border:#fde68a;
--danger:#dc2626; --danger-light:#fef2f2; --danger-border:#fecaca; --danger:#dc2626; --danger-light:#fef2f2; --danger-border:#fecaca;
--violet:#7c3aed; --violet-light:#f5f3ff; --violet-border:#ddd6fe;
--text-primary:#0f172a; --text-secondary:#475569; --text-muted:#94a3b8; --text-primary:#0f172a; --text-secondary:#475569; --text-muted:#94a3b8;
--surface:#ffffff; --surface-secondary:#f8fafc; --border:#e2e8f0; --surface:#ffffff; --surface-secondary:#f8fafc; --border:#e2e8f0;
--shadow-md:0 4px 20px rgba(0,0,0,0.08), 0 2px 6px rgba(0,0,0,0.04); --shadow-md:0 4px 20px rgba(0,0,0,0.08);
--shadow-focus:0 0 0 3px rgba(37,99,235,0.12);
--radius-sm:6px; --radius-md:10px; --radius-lg:14px; --radius-xl:18px; --radius-sm:6px; --radius-md:10px; --radius-lg:14px; --radius-xl:18px;
} }
* { font-family:'Plus Jakarta Sans',sans-serif; box-sizing:border-box; } *{font-family:'Plus Jakarta Sans',sans-serif;box-sizing:border-box;}
.page-wrapper { animation:fadeUp 0.4s ease both; } .page-wrapper{animation:fadeUp 0.4s ease both;}
@keyframes fadeUp { from{opacity:0;transform:translateY(12px)} to{opacity:1;transform:translateY(0)} } @keyframes fadeUp{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}
/* HEADER */ /* HEADER */
.page-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:28px; gap:16px; flex-wrap:wrap; } .page-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:24px;gap:16px;flex-wrap:wrap;}
.page-header-left { display:flex; align-items:center; gap:14px; } .page-header-left{display:flex;align-items:center;gap:14px;}
.page-icon { width:50px; height:50px; border-radius:var(--radius-lg); background:var(--primary-light); border:1px solid var(--primary-border); display:flex; align-items:center; justify-content:center; flex-shrink:0; } .page-icon{width:50px;height:50px;border-radius:var(--radius-lg);background:var(--primary-light);border:1px solid var(--primary-border);display:flex;align-items:center;justify-content:center;flex-shrink:0;}
.page-icon svg { width:24px; height:24px; color:var(--primary); } .page-icon svg{width:24px;height:24px;color:var(--primary);}
.page-title { font-size:22px; font-weight:700; color:var(--text-primary); margin:0 0 2px; letter-spacing:-0.3px; } .page-title{font-size:22px;font-weight:700;color:var(--text-primary);margin:0 0 2px;}
.page-subtitle { font-size:13px; color:var(--text-secondary); margin:0; } .page-subtitle{font-size:13px;color:var(--text-secondary);margin:0;}
/* BUTTON ADD */ /* BUTTONS — ikon dikunci 14px agar tidak meluap */
.btn-add { .btn-primary{display:inline-flex;align-items:center;gap:7px;padding:10px 18px;font-size:13px;font-weight:600;font-family:'Plus Jakarta Sans',sans-serif;background:var(--primary);color:#fff;border:none;border-radius:var(--radius-md);text-decoration:none;cursor:pointer;transition:all .15s;box-shadow:0 2px 8px rgba(37,99,235,0.25);}
display:inline-flex; align-items:center; gap:7px; padding:10px 20px; .btn-primary:hover{background:var(--primary-dark);color:#fff;text-decoration:none;transform:translateY(-1px);}
font-size:13.5px; font-weight:600; font-family:'Plus Jakarta Sans',sans-serif; .btn-primary svg{width:14px;height:14px;flex-shrink:0;}
background:var(--primary); color:#fff; border:none; border-radius:var(--radius-md);
text-decoration:none; cursor:pointer; transition:all 0.15s; .btn-success{display:inline-flex;align-items:center;gap:6px;padding:9px 16px;font-size:13px;font-weight:600;font-family:'Plus Jakarta Sans',sans-serif;background:var(--success);color:#fff;border:none;border-radius:var(--radius-md);cursor:pointer;transition:all .15s;box-shadow:0 2px 8px rgba(22,163,74,0.25);}
box-shadow:0 2px 8px rgba(37,99,235,0.25); .btn-success:hover{background:#15803d;transform:translateY(-1px);}
} .btn-success svg{width:14px;height:14px;flex-shrink:0;}
.btn-add:hover { background:var(--primary-dark); color:#fff; text-decoration:none; transform:translateY(-1px); }
.btn-add svg { width:15px; height:15px; } .btn-violet{display:inline-flex;align-items:center;gap:6px;padding:9px 16px;font-size:13px;font-weight:600;font-family:'Plus Jakarta Sans',sans-serif;background:var(--violet);color:#fff;border:none;border-radius:var(--radius-md);cursor:pointer;transition:all .15s;box-shadow:0 2px 8px rgba(124,58,237,0.25);}
.btn-violet:hover{background:#6d28d9;transform:translateY(-1px);}
.btn-violet svg{width:14px;height:14px;flex-shrink:0;}
.btn-warning-sm{display:inline-flex;align-items:center;gap:5px;padding:5px 12px;font-size:12px;font-weight:600;font-family:'Plus Jakarta Sans',sans-serif;background:var(--warning-light);color:var(--warning);border:1px solid var(--warning-border);border-radius:var(--radius-sm);text-decoration:none;cursor:pointer;transition:all .15s;}
.btn-warning-sm:hover{background:#fef3c7;color:var(--warning);text-decoration:none;}
.btn-warning-sm svg{width:13px;height:13px;flex-shrink:0;}
/* TOAST ALERT — menggantikan alert full-page */
.toast-container{position:fixed;top:20px;right:20px;z-index:9999;display:flex;flex-direction:column;gap:10px;}
.toast{display:flex;align-items:center;gap:12px;padding:14px 18px;border-radius:var(--radius-lg);font-size:13.5px;font-weight:600;max-width:360px;box-shadow:0 8px 30px rgba(0,0,0,0.12);animation:toastIn 0.35s cubic-bezier(.34,1.56,.64,1) both;}
@keyframes toastIn{from{opacity:0;transform:translateX(60px)}to{opacity:1;transform:translateX(0)}}
.toast.toast-success{background:#fff;border:1px solid var(--success-border);color:var(--success);}
.toast-icon{width:32px;height:32px;border-radius:50%;background:var(--success-light);display:flex;align-items:center;justify-content:center;flex-shrink:0;}
.toast-icon svg{width:16px;height:16px;color:var(--success);}
.toast-close{margin-left:auto;background:none;border:none;cursor:pointer;color:var(--text-muted);padding:2px;display:flex;align-items:center;border-radius:4px;transition:color .15s;}
.toast-close:hover{color:var(--text-primary);}
.toast-close svg{width:14px;height:14px;}
/* ALERT inline (fallback jika tidak pakai toast) */
.alert-success{display:flex;align-items:center;gap:10px;background:var(--success-light);border:1px solid var(--success-border);border-radius:var(--radius-md);padding:12px 16px;margin-bottom:18px;font-size:13px;font-weight:600;color:var(--success);}
.alert-success svg{width:16px;height:16px;flex-shrink:0;}
/* FILTER BAR */ /* FILTER BAR */
.filter-bar { display:flex; align-items:center; gap:12px; margin-bottom:0; } .filter-row{display:flex;align-items:center;gap:10px;margin-bottom:18px;flex-wrap:wrap;}
.filter-select { .filter-select{padding:8px 32px 8px 12px;font-size:13px;font-family:'Plus Jakarta Sans',sans-serif;border:1px solid var(--border);border-radius:var(--radius-md);background:var(--surface);color:var(--text-primary);outline:none;appearance:none;cursor:pointer;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%2394a3b8' stroke-width='2'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M19 9l-7 7-7-7'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 10px center;background-size:14px;transition:border-color .2s;}
padding:8px 34px 8px 12px; font-size:13px; font-family:'Plus Jakarta Sans',sans-serif; .filter-select:focus{border-color:var(--primary);}
border:1px solid var(--border); border-radius:var(--radius-md); .filter-label{font-size:13px;font-weight:600;color:var(--text-secondary);}
background:var(--surface); color:var(--text-primary); outline:none;
appearance:none; cursor:pointer; width:200px;
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%2394a3b8' stroke-width='2'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M19 9l-7 7-7-7'/%3E%3C/svg%3E");
background-repeat:no-repeat; background-position:right 10px center; background-size:14px;
transition:border-color 0.2s, box-shadow 0.2s;
}
.filter-select:focus { border-color:var(--primary); box-shadow:var(--shadow-focus); }
.filter-label { font-size:13px; font-weight:500; color:var(--text-secondary); }
/* CARD */ /* CARD */
.section-card { background:var(--surface); border:1px solid var(--border); border-radius:var(--radius-xl); box-shadow:var(--shadow-md); overflow:hidden; } .section-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-xl);box-shadow:var(--shadow-md);overflow:hidden;margin-bottom:22px;}
.card-toolbar { 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; } .card-toolbar{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:10px;}
.toolbar-title { font-size:13.5px; font-weight:600; color:var(--text-primary); } .toolbar-title{font-size:13.5px;font-weight:600;color:var(--text-primary);display:flex;align-items:center;gap:8px;}
.toolbar-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 10px; margin-left:8px; } .toolbar-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 10px;}
/* TABLE */ /* TABLE */
.table-responsive { overflow-x:auto; } .table-responsive{overflow-x:auto;}
table.nilai-table { width:100%; border-collapse:collapse; min-width:600px; } table.nilai-table{width:100%;border-collapse:collapse;min-width:700px;}
.nilai-table thead tr { background:var(--surface-secondary); border-bottom:1px solid var(--border); } .nilai-table thead tr{background:var(--surface-secondary);border-bottom:1px solid var(--border);}
.nilai-table thead th { padding:12px 16px; font-size:11px; font-weight:700; color:var(--text-muted); text-transform:uppercase; letter-spacing:0.7px; text-align:left; white-space:nowrap; } .nilai-table thead th{padding:11px 16px;font-size:11px;font-weight:700;color:var(--text-muted);text-transform:uppercase;letter-spacing:.7px;text-align:left;white-space:nowrap;}
.nilai-table tbody tr { border-bottom:1px solid var(--border); transition:background 0.15s; } .nilai-table thead th.c{text-align:center;}
.nilai-table tbody tr:last-child { border-bottom:none; } .nilai-table tbody tr{border-bottom:1px solid var(--border);transition:background .15s;}
.nilai-table tbody tr:hover { background:#f8faff; } .nilai-table tbody tr:last-child{border-bottom:none;}
.nilai-table tbody td { padding:14px 16px; font-size:13.5px; color:var(--text-primary); vertical-align:middle; } .nilai-table tbody tr:hover{background:#f8faff;}
.cell-no { font-size:13px; color:var(--text-muted); font-weight:500; } .nilai-table tbody td{padding:13px 16px;font-size:13.5px;color:var(--text-primary);vertical-align:middle;}
.nilai-table tbody td.c{text-align:center;}
/* NAME CELL */ /* BADGES */
.name-cell { display:flex; align-items:center; gap:10px; } .name-cell{display:flex;align-items:center;gap:10px;}
.name-avatar { width:34px; height:34px; border-radius:50%; background:var(--primary-light); border:1px solid var(--primary-border); display:flex; align-items:center; justify-content:center; font-size:11px; font-weight:700; color:var(--primary); flex-shrink:0; } .name-avatar{width:34px;height:34px;border-radius:50%;background:var(--primary-light);border:1px solid var(--primary-border);display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;color:var(--primary);flex-shrink:0;}
.badge-kelas{background:var(--primary-light);color:var(--primary);border:1px solid var(--primary-border);border-radius:999px;font-size:11.5px;font-weight:600;padding:3px 11px;display:inline-flex;}
/* NILAI BADGE */ .chip-nilai{display:inline-flex;align-items:center;justify-content:center;min-width:46px;padding:4px 10px;border-radius:var(--radius-sm);font-size:13px;font-weight:700;}
.nilai-chip { .chip-a{background:var(--success-light);color:var(--success);border:1px solid var(--success-border);}
display:inline-flex; align-items:center; justify-content:center; .chip-b{background:var(--primary-light);color:var(--primary);border:1px solid var(--primary-border);}
width:46px; height:28px; border-radius:var(--radius-sm); .chip-c{background:var(--warning-light);color:var(--warning);border:1px solid var(--warning-border);}
font-size:13px; font-weight:700; .chip-d{background:var(--danger-light);color:var(--danger);border:1px solid var(--danger-border);}
} .chip-null{background:var(--surface-secondary);color:var(--text-muted);border:1px solid var(--border);}
.nilai-chip.tinggi { background:var(--success-light); color:var(--success); border:1px solid var(--success-border); } .badge-kirim{background:var(--success-light);color:var(--success);border:1px solid var(--success-border);border-radius:999px;font-size:11px;font-weight:700;padding:2px 9px;display:inline-flex;align-items:center;gap:4px;}
.nilai-chip.sedang { background:var(--warning-light); color:var(--warning); border:1px solid var(--warning-border); } .badge-kirim::before{content:'';width:6px;height:6px;border-radius:50%;background:var(--success);}
.nilai-chip.rendah { background:var(--danger-light); color:var(--danger); border:1px solid var(--danger-border); } .badge-belum{background:var(--surface-secondary);color:var(--text-muted);border:1px solid var(--border);border-radius:999px;font-size:11px;font-weight:600;padding:2px 9px;display:inline-flex;}
.badge-kelas { background:var(--primary-light); color:var(--primary); border:1px solid var(--primary-border); border-radius:999px; font-size:11.5px; font-weight:600; padding:3px 11px; display:inline-flex; }
.badge-mapel { background:var(--surface-secondary); color:var(--text-secondary); border:1px solid var(--border); border-radius:999px; font-size:11.5px; font-weight:600; padding:3px 11px; display:inline-flex; }
.btn-edit-sm {
display:inline-flex; align-items:center; gap:5px; padding:5px 12px;
font-size:12.5px; font-weight:600; font-family:'Plus Jakarta Sans',sans-serif;
background:var(--warning-light); color:var(--warning);
border:1px solid var(--warning-border); border-radius:var(--radius-sm);
text-decoration:none; transition:all 0.15s;
}
.btn-edit-sm:hover { background:#fef3c7; color:var(--warning); text-decoration:none; }
.btn-edit-sm svg { width:13px; height:13px; }
/* EMPTY */ /* EMPTY */
.empty-state { padding:64px 24px; text-align:center; } .empty-state{padding:56px 24px;text-align:center;}
.empty-icon { width:60px; height:60px; background:var(--surface-secondary); border-radius:50%; border:1px solid var(--border); display:flex; align-items:center; justify-content:center; margin:0 auto 14px; } .empty-icon{width:56px;height:56px;background:var(--surface-secondary);border-radius:50%;border:1px solid var(--border);display:flex;align-items:center;justify-content:center;margin:0 auto 12px;}
.empty-icon svg { width:26px; height:26px; color:var(--text-muted); } .empty-icon svg{width:24px;height:24px;color:var(--text-muted);}
.empty-title { font-size:15px; font-weight:600; color:var(--text-primary); margin-bottom:4px; } .empty-title{font-size:15px;font-weight:600;color:var(--text-primary);margin-bottom:4px;}
.empty-desc { font-size:13px; color:var(--text-muted); } .empty-desc{font-size:13px;color:var(--text-muted);}
/* TABLE FOOTER */ /* TABLE FOOTER */
.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); flex-wrap:wrap; gap:6px; } .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);flex-wrap:wrap;gap:6px;}
.table-footer .btn-violet svg{width:13px;height:13px;}
/* INFO BOX */
.info-box{display:flex;align-items:flex-start;gap:10px;background:var(--primary-light);border:1px solid var(--primary-border);border-radius:var(--radius-md);padding:12px 16px;margin-bottom:18px;font-size:13px;color:var(--primary);}
.info-box svg{width:16px;height:16px;flex-shrink:0;margin-top:1px;}
</style> </style>
{{-- TOAST ALERT (pojok kanan atas, auto-dismiss 4 detik) --}}
@if(session('success'))
<div class="toast-container" id="toastContainer">
<div class="toast toast-success" id="toastSuccess">
<div class="toast-icon">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/>
</svg>
</div>
<span>{{ session('success') }}</span>
<button class="toast-close" onclick="dismissToast()">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
</div>
<script>
function dismissToast(){
var t=document.getElementById('toastSuccess');
if(t){t.style.transition='all .3s ease';t.style.opacity='0';t.style.transform='translateX(60px)';setTimeout(function(){t.remove();},300);}
}
setTimeout(dismissToast, 4000);
</script>
@endif
<div class="container-fluid px-4 py-2 page-wrapper"> <div class="container-fluid px-4 py-2 page-wrapper">
{{-- HEADER --}} {{-- HEADER --}}
@ -119,35 +150,78 @@
</svg> </svg>
</div> </div>
<div> <div>
<h4 class="page-title">Data Nilai Siswa</h4> <h4 class="page-title">Menu Nilai</h4>
<p class="page-subtitle">Kelola dan pantau nilai akademik siswa</p> <p class="page-subtitle">Rekap nilai tugas, UTS, UAS &amp; hitung rata-rata siswa</p>
</div> </div>
</div> </div>
<a href="{{ route('guru.nilai.input') }}" class="btn-add"> <a href="{{ route('guru.nilai.input') }}" class="btn-primary">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"> <svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4"/></svg>
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4"/> Input Nilai Manual
</svg>
Input Nilai
</a> </a>
</div> </div>
{{-- TABLE CARD --}} {{-- INFO --}}
<div class="info-box">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
<span>Nilai tugas &amp; ujian otomatis masuk sini saat diklik "Masukkan ke Menu Nilai" di menu Tugas atau Ujian. Setelah semua terisi, pilih kelas klik <strong>Hitung Rata-Rata</strong> lalu <strong>Kirim ke Siswa</strong>.</span>
</div>
{{-- FILTER --}}
<form method="GET" action="{{ route('guru.nilai.index') }}" id="filterForm">
<div class="filter-row">
<span class="filter-label">Filter Kelas:</span>
<select name="kelas_id" class="filter-select" onchange="document.getElementById('filterForm').submit()" style="width:200px;">
<option value="">Semua Kelas</option>
@foreach($kelas as $k)
<option value="{{ $k->id }}" {{ request('kelas_id') == $k->id ? 'selected' : '' }}>{{ $k->nama_kelas }}</option>
@endforeach
</select>
@if(request('kelas_id'))
<span class="filter-label" style="margin-left:10px;">Filter Siswa:</span>
<select name="siswa_id" class="filter-select" onchange="document.getElementById('filterForm').submit()" style="width:220px;">
<option value="">Semua Siswa</option>
@foreach($siswaList as $s)
<option value="{{ $s->id }}" {{ request('siswa_id') == $s->id ? 'selected' : '' }}>{{ $s->nama }}</option>
@endforeach
</select>
@endif
@if(request('kelas_id') || request('siswa_id'))
<a href="{{ route('guru.nilai.index') }}" style="font-size:12.5px;color:var(--text-muted);font-weight:600;text-decoration:none;"> Reset</a>
@endif
</div>
</form>
{{-- TABEL NILAI --}}
<div class="section-card"> <div class="section-card">
<div class="card-toolbar"> <div class="card-toolbar">
<span class="toolbar-title">Daftar Nilai<span class="toolbar-badge">{{ $nilai->count() }} data</span></span> <span class="toolbar-title">
<form method="GET" action="{{ route('guru.nilai.index') }}" style="margin:0;"> Daftar Nilai
<div class="filter-bar"> <span class="toolbar-badge">{{ $nilai->count() }} data</span>
<span class="filter-label">Filter:</span> </span>
<select name="kelas_id" class="filter-select" onchange="this.form.submit()"> @if(request('kelas_id'))
<option value="">Semua Kelas</option> <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
@foreach($kelas as $k) {{-- TOMBOL HITUNG RATA-RATA --}}
<option value="{{ $k->id }}" {{ request('kelas_id') == $k->id ? 'selected' : '' }}> <form action="{{ route('guru.nilai.hitungRata') }}" method="POST">
{{ $k->nama_kelas }} @csrf
</option> <input type="hidden" name="kelas_id" value="{{ request('kelas_id') }}">
@endforeach <button type="submit" class="btn-success" onclick="return confirm('Hitung rata-rata nilai untuk kelas ini?')">
</select> <svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.2"><path stroke-linecap="round" stroke-linejoin="round" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/></svg>
</div> Hitung Rata-Rata
</form> </button>
</form>
{{-- TOMBOL KIRIM KE KELAS INI --}}
<form action="{{ route('guru.nilai.kirimKeSiswa') }}" method="POST">
@csrf
<input type="hidden" name="kelas_id" value="{{ request('kelas_id') }}">
<button type="submit" class="btn-violet" onclick="return confirm('Kirim nilai ke siswa kelas ini? Siswa akan bisa melihat nilai mereka.')">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"/></svg>
Kirim ke Kelas Ini
</button>
</form>
</div>
@endif
</div> </div>
<div class="table-responsive"> <div class="table-responsive">
@ -157,15 +231,25 @@
<th>No</th> <th>No</th>
<th>Nama Siswa</th> <th>Nama Siswa</th>
<th>Kelas</th> <th>Kelas</th>
<th>Mata Pelajaran</th> <th class="c">Nilai Tugas</th>
<th>Nilai</th> <th class="c">Nilai UTS</th>
<th style="text-align:center;">Aksi</th> <th class="c">Nilai UAS</th>
<th class="c">Rata-Rata</th>
<th class="c">Status Kirim</th>
<th class="c">Aksi</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@forelse($nilai as $n) @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
<tr> <tr>
<td class="cell-no">{{ $loop->iteration }}</td> <td style="font-size:12.5px;color:var(--text-muted);">{{ $loop->iteration }}</td>
<td> <td>
<div class="name-cell"> <div class="name-cell">
<div class="name-avatar">{{ strtoupper(substr($n->siswa->nama ?? 'S', 0, 2)) }}</div> <div class="name-avatar">{{ strtoupper(substr($n->siswa->nama ?? 'S', 0, 2)) }}</div>
@ -173,32 +257,33 @@
</div> </div>
</td> </td>
<td><span class="badge-kelas">{{ $n->siswa->kelas->nama_kelas ?? '—' }}</span></td> <td><span class="badge-kelas">{{ $n->siswa->kelas->nama_kelas ?? '—' }}</span></td>
<td><span class="badge-mapel">{{ $n->mapel->nama_mapel ?? '—' }}</span></td> <td class="c"><span class="chip-nilai {{ $tugasClass }}">{{ $n->nilai_tugas ?? '—' }}</span></td>
<td> <td class="c"><span class="chip-nilai {{ $utsClass }}">{{ $n->nilai_uts ?? '—' }}</span></td>
<span class="nilai-chip {{ $n->nilai >= 80 ? 'tinggi' : ($n->nilai >= 60 ? 'sedang' : 'rendah') }}"> <td class="c"><span class="chip-nilai {{ $uasClass }}">{{ $n->nilai_uas ?? '—' }}</span></td>
{{ $n->nilai }} <td class="c"><span class="chip-nilai {{ $rataClass }}">{{ $rata ?? '—' }}</span></td>
</span> <td class="c">
@if($n->sudah_kirim)
<span class="badge-kirim">Sudah Dikirim</span>
@else
<span class="badge-belum">Belum</span>
@endif
</td> </td>
<td style="text-align:center;"> <td class="c">
<a href="{{ route('guru.nilai.edit', $n->id) }}" class="btn-edit-sm"> <a href="{{ route('guru.nilai.edit', $n->id) }}" class="btn-warning-sm">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> <svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M15.232 5.232l3.536 3.536M9 13l6.586-6.586a2 2 0 012.828 2.828L11.828 15.828a2 2 0 01-1.414.586H8v-2.414a2 2 0 01.586-1.414z"/></svg>
<path stroke-linecap="round" stroke-linejoin="round" d="M15.232 5.232l3.536 3.536M9 13l6.586-6.586a2 2 0 012.828 2.828L11.828 15.828a2 2 0 01-1.414.586H8v-2.414a2 2 0 01.586-1.414z"/>
</svg>
Edit Edit
</a> </a>
</td> </td>
</tr> </tr>
@empty @empty
<tr> <tr>
<td colspan="6"> <td colspan="9">
<div class="empty-state"> <div class="empty-state">
<div class="empty-icon"> <div class="empty-icon">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"> <svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/></svg>
<path stroke-linecap="round" stroke-linejoin="round" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
</svg>
</div> </div>
<div class="empty-title">Belum ada data nilai</div> <div class="empty-title">Belum ada data nilai</div>
<div class="empty-desc">Mulai input nilai dengan klik tombol Input Nilai di atas.</div> <div class="empty-desc">Masukkan nilai dari menu Tugas atau Ujian terlebih dahulu.</div>
</div> </div>
</td> </td>
</tr> </tr>
@ -207,9 +292,20 @@
</table> </table>
</div> </div>
{{-- FOOTER + TOMBOL KIRIM SEMUA KELAS --}}
<div class="table-footer"> <div class="table-footer">
<span>Menampilkan {{ $nilai->count() }} data nilai</span> <span>{{ $nilai->count() }} data nilai</span>
<span>{{ now()->format('d M Y') }}</span> <div style="display:flex;align-items:center;gap:10px;">
<span>{{ now()->format('d M Y') }}</span>
<form action="{{ route('guru.nilai.kirimKeSiswa') }}" method="POST" style="margin:0;">
@csrf
<button type="submit" class="btn-violet" style="padding:7px 14px;font-size:12.5px;"
onclick="return confirm('Kirim nilai ke SEMUA kelas? Siswa di semua kelas akan bisa melihat nilai mereka.')">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"/></svg>
Kirim ke Semua Kelas
</button>
</form>
</div>
</div> </div>
</div> </div>

View File

@ -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); } .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 */ /* ===== FILTER RATA-RATA ===== */
.btn-hitung-rata { .filter-section { margin-top:24px; }
display: inline-flex; align-items: center; gap: 8px; .filter-card {
padding: 10px 22px; border-radius: var(--radius-md); border: none; background:var(--surface); border:1px solid var(--border);
background: var(--success); color: #fff; border-radius:var(--radius-xl); box-shadow:var(--shadow-md); overflow:hidden;
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;
} }
.btn-hitung-rata:hover { background: #15803d; transform: translateY(-1px); } .filter-card-header {
.btn-hitung-rata svg { width: 15px; height: 15px; } 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 { .filter-body { padding:18px 22px; }
background: var(--surface); border: 1px solid var(--border); .filter-select-all-row {
border-radius: var(--radius-xl); box-shadow: var(--shadow-md); overflow: hidden; display:flex; align-items:center; justify-content:space-between;
margin-bottom:14px; flex-wrap:wrap; gap:8px;
} }
.rata-card-toolbar { .filter-select-all-row span { font-size:13px; color:var(--text-secondary); font-weight:500; }
display: flex; align-items: center; justify-content: space-between; .filter-quick-links { display:flex; gap:14px; }
padding: 16px 22px; border-bottom: 1px solid var(--border); .filter-quick-link {
background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%); 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; } .filter-quick-link:hover { text-decoration:underline; }
.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; }
table.rata-table { width: 100%; border-collapse: collapse; min-width: 600px; } .kelas-group { margin-bottom:12px; border:1px solid var(--border); border-radius:var(--radius-lg); overflow:hidden; }
.rata-table thead tr { .kelas-group:last-child { margin-bottom:0; }
background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%); .kelas-group-header {
border-bottom: 2px solid #bbf7d0; 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 { .kelas-group-header:hover { background:#f1f5f9; }
padding: 12px 16px; font-size: 11px; font-weight: 700; .kelas-group-header input[type=checkbox] {
color: #166534; text-transform: uppercase; letter-spacing: .7px; width:16px; height:16px; accent-color:var(--primary); cursor:pointer; flex-shrink:0;
white-space: nowrap;
} }
.rata-table thead th.center { text-align: center; } .kelas-label-name { font-size:13.5px; font-weight:700; color:var(--text-primary); flex:1; }
.rata-table tbody tr { border-bottom: 1px solid var(--border); transition: background .15s; } .kelas-count-badge {
.rata-table tbody tr:last-child { border-bottom: none; } background:var(--primary-light); color:var(--primary);
.rata-table tbody tr:hover { background: #f0fdf4; } border:1px solid var(--primary-border); border-radius:999px;
.rata-table tbody td { padding: 14px 16px; font-size: 13.5px; color: var(--text-primary); vertical-align: middle; } font-size:11.5px; font-weight:600; padding:2px 9px;
.rata-table tbody td.center { text-align: center; } }
.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 { .rata-avatar {
width: 36px; height: 36px; border-radius: 50%; width:34px; height:34px; border-radius:50%;
background: var(--success-light); border: 2px solid #bbf7d0; background:var(--success-light); border:1.5px solid var(--success-border);
display: flex; align-items: center; justify-content: center; display:flex; align-items:center; justify-content:center;
font-size: 13px; font-weight: 800; color: var(--success); flex-shrink: 0; 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-lg {
display:inline-flex; padding:5px 16px; border-radius:999px;
.chip-nilai { font-size:14px; font-weight:800;
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 { .chip-predikat {
display: inline-flex; align-items: center; gap: 4px; display:inline-flex; padding:4px 11px; border-radius:999px;
padding: 4px 12px; border-radius: 999px; font-size: 11.5px; font-weight: 700; 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 { .rata-result-footer {
display: flex; align-items: center; justify-content: space-between; display:flex; align-items:center; justify-content:space-between;
padding: 13px 22px; border-top: 1px solid var(--border); padding:13px 22px; border-top:1px solid var(--border);
background: var(--surface-secondary); font-size: 13px; color: var(--text-muted); 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); }
</style> </style>
<div class="container-fluid px-4 py-2 page-wrapper"> <div class="container-fluid px-4 py-2 page-wrapper">
@ -192,6 +292,7 @@
</a> </a>
</div> </div>
{{-- TABEL DAFTAR TUGAS --}}
<div class="section-card"> <div class="section-card">
<div class="card-toolbar"> <div class="card-toolbar">
<span class="toolbar-title">Daftar Tugas<span class="toolbar-badge">{{ $tugas->count() }} tugas</span></span> <span class="toolbar-title">Daftar Tugas<span class="toolbar-badge">{{ $tugas->count() }} tugas</span></span>
@ -279,103 +380,330 @@
@endif @endif
</div> </div>
{{-- TOMBOL HITUNG --}} {{-- ===== FILTER RATA-RATA PER KELAS & SISWA ===== --}}
<div style="text-align:right"> <div class="filter-section">
<button class="btn-hitung-rata" onclick="tampilRataRata()"> <div class="filter-card">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.2"> <div class="filter-card-header">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/> <div class="filter-header-left">
</svg> <div class="filter-header-icon">
Hitung Rata-Rata Nilai Siswa <svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
</button> <path stroke-linecap="round" stroke-linejoin="round" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2a1 1 0 01-.293.707L13 13.414V19a1 1 0 01-.553.894l-4 2A1 1 0 017 21v-7.586L3.293 6.707A1 1 0 013 6V4z"/>
</div> </svg>
</div>
{{-- TABEL RATA-RATA --}} <div>
<div id="rata-section" class="rata-section" style="display:none"> <p class="filter-header-title">Filter Perhitungan Rata-Rata Nilai</p>
<div class="rata-card"> <p class="filter-header-sub">Pilih kelas dan siswa yang ingin dihitung rata-rata nilainya</p>
<div class="rata-card-toolbar"> </div>
<div class="rata-toolbar-left"> </div>
<div class="rata-toolbar-icon"> <button class="btn-hitung-rata2" onclick="hitungRataRata()">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.2"> <svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/> <path stroke-linecap="round" stroke-linejoin="round" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
</svg> </svg>
Hitung Rata-Rata
</button>
</div>
<div class="filter-body">
<div class="filter-select-all-row">
<span>Centang siswa yang ingin dihitung:</span>
<div class="filter-quick-links">
<button class="filter-quick-link" onclick="selectAllSiswa(true)">Pilih Semua</button>
<button class="filter-quick-link" onclick="selectAllSiswa(false)">Hapus Semua</button>
</div>
</div> </div>
<div> <div id="filter-kelas-list">
<p class="rata-toolbar-title">Rekap Rata-Rata Nilai per Siswa</p> {{-- Dibangun oleh JS dari data PHP --}}
<p class="rata-toolbar-sub">Dihitung dari semua tugas yang sudah dinilai guru</p>
</div> </div>
</div> </div>
</div> </div>
<div class="table-responsive">
<table class="rata-table">
<thead>
<tr>
<th style="width:36px">#</th>
<th>Nama Siswa</th>
<th class="center">Jumlah Tugas Dinilai</th>
<th class="center">Rata-Rata Nilai</th>
<th class="center">Predikat</th>
</tr>
</thead>
<tbody>
@forelse($rekapNilai as $i => $r)
<tr>
<td style="color:var(--text-muted);font-size:12px">{{ $i + 1 }}</td>
<td>
<div class="rata-avatar-row">
<div class="rata-avatar">{{ $r['inisial'] }}</div>
<span class="rata-nama">{{ $r['nama'] }}</span>
</div>
</td>
<td class="center" style="color:var(--text-secondary)">
{{ $r['jumlah_tugas'] }} tugas
</td>
<td class="center">
@php
$n = $r['rata_rata'];
$cls = $n >= 88 ? 'a' : ($n >= 75 ? 'b' : ($n >= 60 ? 'c' : 'd'));
@endphp
<span class="chip-nilai {{ $cls }}">{{ $n }}</span>
</td>
<td class="center">
@php
$predikat = $n >= 88 ? 'A — Sangat Baik'
: ($n >= 75 ? 'B — Baik'
: ($n >= 60 ? 'C — Cukup' : 'D — Kurang'));
@endphp
<span class="chip-predikat {{ $cls }}">{{ $predikat }}</span>
</td>
</tr>
@empty
<tr>
<td colspan="5" style="text-align:center;padding:40px;color:var(--text-muted);font-size:13px">
Belum ada siswa yang mendapatkan nilai
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@if($rekapNilai->count())
<div class="rata-footer">
<span>{{ $rekapNilai->count() }} siswa terhitung</span>
<div style="display:flex;align-items:center;gap:8px">
<span>Rata-rata kelas:</span>
<span class="rata-footer-nilai">
{{ round($rekapNilai->avg('rata_rata'), 1) }} / 100
</span>
</div>
</div>
@endif
</div> </div>
{{-- TABEL HASIL RATA-RATA --}}
<div id="rata-result-section" class="rata-result-section" style="display:none"></div>
</div> </div>
{{-- DATA REKAP DARI PHP JS --}}
<script> <script>
function tampilRataRata() { const REKAP_DATA = @json($rekapPerKelas);
const sec = document.getElementById('rata-section'); const CSRF_TOKEN = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
function getChipClass(n) {
if (n >= 88) return 'a';
if (n >= 75) return 'b';
if (n >= 60) return 'c';
return 'd';
}
function getPredikat(n) {
if (n >= 88) return 'A — Sangat Baik';
if (n >= 75) return 'B — Baik';
if (n >= 60) return 'C — Cukup';
return 'D — Kurang';
}
function buildFilter() {
const container = document.getElementById('filter-kelas-list');
if (!container) return;
const entries = Object.entries(REKAP_DATA);
if (entries.length === 0) {
container.innerHTML = '<p style="text-align:center;padding:24px;font-size:13px;color:var(--text-muted)">Belum ada siswa yang mendapatkan nilai.</p>';
return;
}
entries.forEach(([kelas, siswaList]) => {
if (!siswaList || siswaList.length === 0) return;
const avgKelas = siswaList.length
? Math.round(siswaList.reduce((s, x) => s + parseFloat(x.rata_rata), 0) / siswaList.length)
: 0;
const grp = document.createElement('div');
grp.className = 'kelas-group';
grp.dataset.kelas = kelas;
const header = document.createElement('div');
header.className = 'kelas-group-header';
const cbKelas = document.createElement('input');
cbKelas.type = 'checkbox';
cbKelas.checked = true;
cbKelas.className = 'cb-kelas';
cbKelas.dataset.kelas = kelas;
cbKelas.addEventListener('change', (e) => {
grp.querySelectorAll('.cb-siswa').forEach(cb => { cb.checked = e.target.checked; });
});
const labelName = document.createElement('span');
labelName.className = 'kelas-label-name';
labelName.textContent = kelas;
const badge = document.createElement('span');
badge.className = 'kelas-count-badge';
badge.textContent = siswaList.length + ' siswa';
const avgPreview = document.createElement('span');
avgPreview.className = 'kelas-avg-preview';
avgPreview.textContent = 'Rata kelas: ' + avgKelas;
const toggleBtn = document.createElement('button');
toggleBtn.className = 'kelas-toggle-btn';
toggleBtn.innerHTML = '▲';
toggleBtn.title = 'Tampilkan/sembunyikan';
header.append(cbKelas, labelName, badge, avgPreview, toggleBtn);
const listWrap = document.createElement('div');
listWrap.className = 'siswa-list-wrapper';
toggleBtn.addEventListener('click', (e) => {
e.preventDefault();
const hidden = listWrap.style.display === 'none';
listWrap.style.display = hidden ? 'block' : 'none';
toggleBtn.innerHTML = hidden ? '▲' : '▼';
});
header.addEventListener('click', (e) => {
if (e.target === cbKelas || e.target === toggleBtn) return;
toggleBtn.click();
});
siswaList.forEach(s => {
const cls = getChipClass(parseFloat(s.rata_rata));
const item = document.createElement('label');
item.className = 'siswa-item';
item.style.cursor = 'pointer';
const cbSiswa = document.createElement('input');
cbSiswa.type = 'checkbox';
cbSiswa.checked = true;
cbSiswa.className = 'cb-siswa';
cbSiswa.dataset.siswaId = s.id;
cbSiswa.dataset.kelas = kelas;
cbSiswa.addEventListener('change', () => {
const all = [...grp.querySelectorAll('.cb-siswa')];
const cbK = grp.querySelector('.cb-kelas');
const allChecked = all.every(cb => cb.checked);
const someChecked = all.some(cb => cb.checked);
cbK.checked = allChecked;
cbK.indeterminate = !allChecked && someChecked;
});
const avatar = document.createElement('div');
avatar.className = 'siswa-avatar';
avatar.textContent = s.inisial;
const nama = document.createElement('span');
nama.className = 'siswa-nama-text';
nama.textContent = s.nama;
const tugasInfo = document.createElement('span');
tugasInfo.className = 'siswa-tugas-text';
tugasInfo.textContent = s.jumlah_tugas + ' tugas';
const chip = document.createElement('span');
chip.className = 'chip-nilai-sm chip-' + cls;
chip.textContent = parseFloat(s.rata_rata).toFixed(1);
// ── TOMBOL MASUKKAN KE MENU NILAI (baru) ──────────────
const formMasuk = document.createElement('form');
formMasuk.action = '/guru/nilai/masukkan-tugas';
formMasuk.method = 'POST';
formMasuk.style.cssText = 'display:inline-flex;flex-shrink:0;';
formMasuk.innerHTML = `
<input type="hidden" name="_token" value="${CSRF_TOKEN}">
<input type="hidden" name="siswa_id" value="${s.id}">
<input type="hidden" name="nilai_tugas" value="${parseFloat(s.rata_rata).toFixed(2)}">
<button type="submit" class="btn-masuk-nilai"
onclick="event.stopPropagation(); return confirm('Masukkan nilai tugas ${s.nama.replace(/'/g,"\\'")} (${parseFloat(s.rata_rata).toFixed(1)}) ke Menu Nilai?')">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.2" style="width:11px;height:11px;">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10"/>
</svg>
Menu Nilai
</button>
`;
item.append(cbSiswa, avatar, nama, tugasInfo, chip, formMasuk);
listWrap.appendChild(item);
});
grp.appendChild(header);
grp.appendChild(listWrap);
container.appendChild(grp);
});
}
function selectAllSiswa(val) {
document.querySelectorAll('.cb-siswa, .cb-kelas').forEach(cb => {
cb.checked = val;
cb.indeterminate = false;
});
}
function hitungRataRata() {
const selected = {};
document.querySelectorAll('.cb-siswa:checked').forEach(cb => {
const kelas = cb.dataset.kelas;
const id = parseInt(cb.dataset.siswaId);
if (!selected[kelas]) selected[kelas] = [];
selected[kelas].push(id);
});
const sec = document.getElementById('rata-result-section');
if (Object.keys(selected).length === 0) {
sec.innerHTML = '<div class="rata-result-card"><div class="rata-empty">Pilih minimal satu siswa terlebih dahulu.</div></div>';
sec.style.display = 'block';
sec.scrollIntoView({ behavior: 'smooth', block: 'start' });
return;
}
let html = '<div class="rata-result-card">';
html += `<div class="rata-result-header">
<div class="rata-result-icon">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
</svg>
</div>
<div>
<p class="rata-result-title">Rekap Rata-Rata Nilai per Siswa</p>
<p class="rata-result-sub">Dihitung dari semua tugas yang sudah dinilai, dikelompokkan per kelas</p>
</div>
</div>`;
const allAvgs = [];
let totalSiswa = 0;
let totalKelas = 0;
Object.entries(REKAP_DATA).forEach(([kelas, siswaList]) => {
const selectedIds = selected[kelas] || [];
const filtered = siswaList
.filter(s => selectedIds.includes(parseInt(s.id)))
.sort((a, b) => parseFloat(b.rata_rata) - parseFloat(a.rata_rata));
if (filtered.length === 0) return;
totalKelas++;
totalSiswa += filtered.length;
const kelasAvgArr = filtered.map(s => parseFloat(s.rata_rata));
const kelasAvg = Math.round(kelasAvgArr.reduce((a, b) => a + b, 0) / kelasAvgArr.length);
allAvgs.push(...kelasAvgArr);
const cls = getChipClass(kelasAvg);
html += `<div class="kelas-result-block">
<div class="kelas-result-header">
<span class="kelas-result-name">${kelas} &nbsp;·&nbsp; ${filtered.length} siswa</span>
<span class="kelas-result-avg">
Rata-rata kelas:&nbsp;
<span class="chip-nilai-sm chip-${cls}" style="font-size:13px">${kelasAvg}</span>
</span>
</div>
<div class="table-responsive">
<table class="rata-result-table">
<thead>
<tr>
<th style="width:32px">#</th>
<th>Nama Siswa</th>
<th class="c">Tugas Dinilai</th>
<th class="c">Rata-Rata Nilai</th>
<th class="c">Predikat</th>
<th class="c">Aksi</th>
</tr>
</thead>
<tbody>`;
filtered.forEach((s, i) => {
const rataS = Math.round(parseFloat(s.rata_rata) * 10) / 10;
const clsS = getChipClass(rataS);
html += `<tr>
<td style="font-size:12px;color:var(--text-muted)">${i + 1}</td>
<td>
<div class="rata-avatar-row">
<div class="rata-avatar">${s.inisial}</div>
<span style="font-weight:600">${s.nama}</span>
</div>
</td>
<td class="c" style="color:var(--text-secondary)">${s.jumlah_tugas} tugas</td>
<td class="c"><span class="chip-nilai-lg chip-${clsS}">${rataS}</span></td>
<td class="c"><span class="chip-predikat chip-${clsS}">${getPredikat(rataS)}</span></td>
<td class="c">
<form action="/guru/nilai/masukkan-tugas" method="POST" style="display:inline;">
<input type="hidden" name="_token" value="${CSRF_TOKEN}">
<input type="hidden" name="siswa_id" value="${s.id}">
<input type="hidden" name="nilai_tugas" value="${rataS}">
<button type="submit" class="btn-masuk-nilai"
onclick="return confirm('Masukkan nilai tugas ${s.nama.replace(/'/g,"\\'")} (${rataS}) ke Menu Nilai?')">
Menu Nilai
</button>
</form>
</td>
</tr>`;
});
html += `</tbody></table></div></div>`;
});
const totalAvg = allAvgs.length
? (Math.round(allAvgs.reduce((a, b) => a + b, 0) / allAvgs.length * 10) / 10)
: 0;
html += `<div class="rata-result-footer">
<span>${totalSiswa} siswa dari ${totalKelas} kelas</span>
<div style="display:flex;align-items:center;gap:10px">
<span>Rata-rata keseluruhan:</span>
<span class="rata-result-footer-val">${totalAvg} / 100</span>
</div>
</div>`;
html += '</div>';
sec.innerHTML = html;
sec.style.display = 'block'; sec.style.display = 'block';
sec.scrollIntoView({ behavior: 'smooth', block: 'start' }); sec.scrollIntoView({ behavior: 'smooth', block: 'start' });
} }
document.addEventListener('DOMContentLoaded', buildFilter);
</script> </script>
</div>
@endsection @endsection

View File

@ -33,6 +33,9 @@
.sw-btn-success:hover{background:#047857;color:#fff;transform:translateY(-1px);} .sw-btn-success:hover{background:#047857;color:#fff;transform:translateY(-1px);}
.sw-btn-info{background:var(--info-light);color:var(--info);border:1px solid #BAE6FD;} .sw-btn-info{background:var(--info-light);color:var(--info);border:1px solid #BAE6FD;}
.sw-btn-info:hover{background:var(--info);color:#fff;} .sw-btn-info:hover{background:var(--info);color:#fff;}
/* Tombol baru: kirim ke menu nilai */
.sw-btn-nilai{background:#f5f3ff;color:#7c3aed;border:1px solid #ddd6fe;font-size:12px;}
.sw-btn-nilai:hover{background:#7c3aed;color:#fff;}
.sw-card{background:var(--surface);border-radius:var(--radius-lg);border:1px solid var(--border);box-shadow:0 1px 3px rgba(0,0,0,.05);overflow:hidden;margin-bottom:22px;} .sw-card{background:var(--surface);border-radius:var(--radius-lg);border:1px solid var(--border);box-shadow:0 1px 3px rgba(0,0,0,.05);overflow:hidden;margin-bottom:22px;}
.sw-card-hd{display:flex;align-items:center;justify-content:space-between;padding:16px 22px;border-bottom:1px solid var(--border);background:linear-gradient(135deg,#F5F3FF 0%,#EEF2FF 100%);} .sw-card-hd{display:flex;align-items:center;justify-content:space-between;padding:16px 22px;border-bottom:1px solid var(--border);background:linear-gradient(135deg,#F5F3FF 0%,#EEF2FF 100%);}
.sw-card-hd-left{display:flex;align-items:center;gap:10px;} .sw-card-hd-left{display:flex;align-items:center;gap:10px;}
@ -54,6 +57,7 @@
.sw-jenis-uas{background:var(--warning-light);border:1px solid #FDE68A;color:var(--warning);} .sw-jenis-uas{background:var(--warning-light);border:1px solid #FDE68A;color:var(--warning);}
.sw-status-draft{display:inline-flex;align-items:center;gap:5px;background:#F3F4F6;border:1px solid #D1D5DB;border-radius:99px;padding:3px 10px;font-size:11px;font-weight:700;color:#6B7280;} .sw-status-draft{display:inline-flex;align-items:center;gap:5px;background:#F3F4F6;border:1px solid #D1D5DB;border-radius:99px;padding:3px 10px;font-size:11px;font-weight:700;color:#6B7280;}
.sw-status-kirim{display:inline-flex;align-items:center;gap:5px;background:var(--success-light);border:1px solid #A7F3D0;border-radius:99px;padding:3px 10px;font-size:11px;font-weight:700;color:var(--success);} .sw-status-kirim{display:inline-flex;align-items:center;gap:5px;background:var(--success-light);border:1px solid #A7F3D0;border-radius:99px;padding:3px 10px;font-size:11px;font-weight:700;color:var(--success);}
.sw-status-nilai{display:inline-flex;align-items:center;gap:5px;background:#f5f3ff;border:1px solid #ddd6fe;border-radius:99px;padding:3px 10px;font-size:11px;font-weight:700;color:#7c3aed;}
.sw-ujian-actions{display:flex;gap:8px;align-items:center;flex-shrink:0;flex-wrap:wrap;} .sw-ujian-actions{display:flex;gap:8px;align-items:center;flex-shrink:0;flex-wrap:wrap;}
.sw-empty{padding:70px 20px;text-align:center;} .sw-empty{padding:70px 20px;text-align:center;}
.sw-empty-ring{width:80px;height:80px;border-radius:50%;background:var(--primary-light);border:2px solid #C7D2FE;display:flex;align-items:center;justify-content:center;margin:0 auto 20px;color:var(--primary);} .sw-empty-ring{width:80px;height:80px;border-radius:50%;background:var(--primary-light);border:2px solid #C7D2FE;display:flex;align-items:center;justify-content:center;margin:0 auto 20px;color:var(--primary);}
@ -63,7 +67,6 @@
/* NILAI SECTION */ /* NILAI SECTION */
.sw-nilai-card{background:var(--surface);border-radius:var(--radius-lg);border:1px solid var(--border);box-shadow:0 1px 3px rgba(0,0,0,.05);overflow:hidden;} .sw-nilai-card{background:var(--surface);border-radius:var(--radius-lg);border:1px solid var(--border);box-shadow:0 1px 3px rgba(0,0,0,.05);overflow:hidden;}
.sw-nilai-hd{display:flex;align-items:center;justify-content:space-between;padding:16px 22px;border-bottom:1px solid var(--border);background:linear-gradient(135deg,#ECFDF5,#D1FAE5);} .sw-nilai-hd{display:flex;align-items:center;justify-content:space-between;padding:16px 22px;border-bottom:1px solid var(--border);background:linear-gradient(135deg,#ECFDF5,#D1FAE5);}
.sw-nilai-filter{display:flex;align-items:center;gap:10px;flex-wrap:wrap;}
.sw-select-kelas{padding:8px 14px;border:1.5px solid var(--border);border-radius:var(--radius-md);font-size:13px;font-weight:600;color:var(--text-dark);background:var(--neutral-light);font-family:'Plus Jakarta Sans',sans-serif;outline:none;} .sw-select-kelas{padding:8px 14px;border:1.5px solid var(--border);border-radius:var(--radius-md);font-size:13px;font-weight:600;color:var(--text-dark);background:var(--neutral-light);font-family:'Plus Jakarta Sans',sans-serif;outline:none;}
.sw-select-kelas:focus{border-color:var(--success);box-shadow:0 0 0 3px rgba(5,150,105,.1);} .sw-select-kelas:focus{border-color:var(--success);box-shadow:0 0 0 3px rgba(5,150,105,.1);}
.sw-table{width:100%;border-collapse:collapse;} .sw-table{width:100%;border-collapse:collapse;}
@ -163,7 +166,7 @@
</div> </div>
<div class="sw-ujian-actions"> <div class="sw-ujian-actions">
{{-- Tombol Kirim (hanya kalau masih draft) --}} {{-- Tombol Kirim ke Siswa (hanya kalau masih draft) --}}
@if($u->status_kirim == 'draft') @if($u->status_kirim == 'draft')
<form action="{{ route('guru.ujian.kirim', $u->id) }}" method="POST" class="d-inline" <form action="{{ route('guru.ujian.kirim', $u->id) }}" method="POST" class="d-inline"
onsubmit="return confirm('Kirim ujian ini ke semua kelas yang dipilih?')"> onsubmit="return confirm('Kirim ujian ini ke semua kelas yang dipilih?')">
@ -175,6 +178,19 @@
</form> </form>
@endif @endif
{{-- Tombol Kirim ke Menu Nilai (hanya kalau sudah terkirim & ada hasil) --}}
@if($u->status_kirim == 'terkirim')
<form action="{{ route('guru.nilai.masukkanUjian') }}" method="POST" class="d-inline"
onsubmit="return confirm('Kirim nilai {{ $u->jenis }} ujian ini ke Menu Nilai? Nilai siswa yang sudah mengerjakan akan masuk ke rekap nilai.')">
@csrf
<input type="hidden" name="ujian_id" value="{{ $u->id }}">
<button type="submit" class="sw-btn sw-btn-sm sw-btn-nilai">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10"/></svg>
Menu Nilai ({{ $u->jenis }})
</button>
</form>
@endif
<a href="{{ route('guru.ujian.edit', $u->id) }}" class="sw-btn sw-btn-sm sw-btn-warning"> <a href="{{ route('guru.ujian.edit', $u->id) }}" class="sw-btn sw-btn-sm sw-btn-warning">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
Edit Edit

View File

@ -6,486 +6,180 @@
@import url('https://fonts.googleapis.com/css2?family=Sora:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Sora:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap');
:root { :root {
--ink: #0c0f1a; --ink:#0c0f1a; --text-sec:#475569; --text-muted:#94a3b8;
--text-sec: #475569; --surface:#ffffff; --surface-2:#f6f8fc; --surface-3:#eef1f8; --border:#e2e8f0;
--text-muted: #94a3b8; --blue:#2563eb; --blue-dark:#1d4ed8; --blue-soft:#eff6ff; --blue-border:#bfdbfe;
--surface: #ffffff; --green:#059669; --green-soft:#ecfdf5; --green-border:#a7f3d0;
--surface-2: #f6f8fc; --amber:#d97706; --amber-soft:#fffbeb; --amber-border:#fde68a;
--surface-3: #eef1f8; --violet:#7c3aed; --violet-soft:#f5f3ff; --violet-border:#ddd6fe;
--border: #e2e8f0; --red:#dc2626; --red-soft:#fef2f2; --red-border:#fecaca;
--r-md:10px; --r-lg:16px; --r-xl:22px; --r-2xl:28px;
--blue: #2563eb; --shadow-card:0 4px 20px rgba(12,15,26,0.07), 0 1px 4px rgba(12,15,26,0.04);
--blue-dark: #1d4ed8;
--blue-soft: #eff6ff;
--blue-border: #bfdbfe;
--green: #059669;
--green-soft: #ecfdf5;
--green-border: #a7f3d0;
--amber: #d97706;
--amber-soft: #fffbeb;
--amber-border: #fde68a;
--violet: #7c3aed;
--violet-soft: #f5f3ff;
--violet-border:#ddd6fe;
--red: #dc2626;
--red-soft: #fef2f2;
--red-border: #fecaca;
--r-md: 10px;
--r-lg: 16px;
--r-xl: 22px;
--r-2xl: 28px;
--shadow-card: 0 4px 20px rgba(12,15,26,0.07), 0 1px 4px rgba(12,15,26,0.04);
--shadow-hover:0 10px 32px rgba(12,15,26,0.11), 0 2px 8px rgba(12,15,26,0.05);
} }
*, *::before, *::after { box-sizing:border-box; margin:0; padding:0; }
.nilai-page { font-family:'Sora',sans-serif; padding:28px 28px 52px; background:var(--surface-2); min-height:100vh; }
.nilai-page > * { animation:rise 0.45s ease both; }
.nilai-page > *:nth-child(1){animation-delay:0s;}
.nilai-page > *:nth-child(2){animation-delay:0.07s;}
.nilai-page > *:nth-child(3){animation-delay:0.14s;}
.nilai-page > *:nth-child(4){animation-delay:0.20s;}
@keyframes rise { from{opacity:0;transform:translateY(14px)} to{opacity:1;transform:translateY(0)} }
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } /* HERO */
.page-hero { background:var(--ink); border-radius:var(--r-2xl); padding:28px 32px; margin-bottom:24px; display:flex; align-items:center; justify-content:space-between; gap:20px; position:relative; overflow:hidden; }
.page-hero::before { content:''; position:absolute; inset:0; background:radial-gradient(ellipse 55% 90% at 100% 50%, rgba(37,99,235,0.22) 0%, transparent 65%), radial-gradient(ellipse 35% 60% at 0% 0%, rgba(124,58,237,0.13) 0%, transparent 55%); pointer-events:none; }
.hero-left { position:relative; z-index:1; }
.hero-tag { display:inline-flex; align-items:center; gap:7px; background:rgba(255,255,255,0.08); border:1px solid rgba(255,255,255,0.13); border-radius:999px; padding:4px 14px; font-size:11.5px; font-weight:600; color:rgba(255,255,255,0.6); letter-spacing:0.6px; text-transform:uppercase; margin-bottom:12px; }
.hero-tag-dot { width:7px; height:7px; border-radius:50%; background:#22c55e; box-shadow:0 0 7px #22c55e; }
.hero-title { font-size:24px; font-weight:800; color:#fff; letter-spacing:-0.4px; line-height:1.2; }
.hero-title em { font-style:normal; color:#93c5fd; }
.hero-sub { font-size:13px; color:rgba(255,255,255,0.45); margin-top:6px; }
.hero-right { position:relative; z-index:1; display:flex; align-items:center; gap:12px; flex-shrink:0; }
.hero-stat { background:rgba(255,255,255,0.07); border:1px solid rgba(255,255,255,0.11); border-radius:var(--r-lg); padding:14px 20px; text-align:center; }
.hero-stat-val { font-size:28px; font-weight:800; color:#fff; letter-spacing:-1px; line-height:1; font-family:'JetBrains Mono',monospace; }
.hero-stat-lbl { font-size:11px; font-weight:600; color:rgba(255,255,255,0.42); text-transform:uppercase; letter-spacing:0.7px; margin-top:4px; }
.nilai-page { /* SUMMARY */
font-family: 'Sora', sans-serif; .summary-grid { display:grid; grid-template-columns:repeat(4,1fr); gap:14px; margin-bottom:22px; }
padding: 28px 28px 52px; .sum-card { background:var(--surface); border:1px solid var(--border); border-radius:var(--r-xl); padding:20px 18px; box-shadow:var(--shadow-card); position:relative; overflow:hidden; transition:box-shadow .2s,transform .2s; }
background: var(--surface-2); .sum-card:hover { transform:translateY(-2px); }
min-height: 100vh; .sum-card::before { content:''; position:absolute; top:0; left:0; right:0; height:3px; border-radius:var(--r-xl) var(--r-xl) 0 0; }
} .sum-card.blue::before { background:linear-gradient(90deg, var(--blue), #60a5fa); }
.sum-card.amber::before { background:linear-gradient(90deg, var(--amber), #fbbf24); }
.sum-card.green::before { background:linear-gradient(90deg, var(--green), #34d399); }
.sum-card.violet::before { background:linear-gradient(90deg, var(--violet), #a78bfa); }
.sum-icon { width:40px; height:40px; border-radius:var(--r-md); display:flex; align-items:center; justify-content:center; margin-bottom:14px; }
.sum-card.blue .sum-icon { background:var(--blue-soft); border:1px solid var(--blue-border); }
.sum-card.amber .sum-icon { background:var(--amber-soft); border:1px solid var(--amber-border); }
.sum-card.green .sum-icon { background:var(--green-soft); border:1px solid var(--green-border); }
.sum-card.violet .sum-icon { background:var(--violet-soft); border:1px solid var(--violet-border); }
.sum-icon svg { width:18px; height:18px; }
.sum-card.blue .sum-icon svg { color:var(--blue); }
.sum-card.amber .sum-icon svg { color:var(--amber); }
.sum-card.green .sum-icon svg { color:var(--green); }
.sum-card.violet .sum-icon svg { color:var(--violet); }
.sum-label { font-size:11px; font-weight:700; color:var(--text-muted); text-transform:uppercase; letter-spacing:0.7px; margin-bottom:4px; }
.sum-value { font-size:26px; font-weight:800; color:var(--ink); letter-spacing:-0.8px; font-family:'JetBrains Mono',monospace; line-height:1; }
.sum-sub { font-size:11.5px; color:var(--text-muted); margin-top:5px; }
/* ── ANIMATIONS ───────────────────────────────────── */ /* TABLE CARD */
.nilai-page > * { animation: rise 0.45s ease both; } .table-card { background:var(--surface); border:1px solid var(--border); border-radius:var(--r-xl); box-shadow:var(--shadow-card); overflow:hidden; }
.nilai-page > *:nth-child(1) { animation-delay: 0s; } .table-toolbar { display:flex; align-items:center; justify-content:space-between; padding:18px 24px 14px; border-bottom:1px solid var(--border); flex-wrap:wrap; gap:12px; }
.nilai-page > *:nth-child(2) { animation-delay: 0.07s; } .toolbar-title { font-size:14px; font-weight:700; color:var(--ink); display:flex; align-items:center; gap:10px; }
.nilai-page > *:nth-child(3) { animation-delay: 0.14s; } .toolbar-icon { width:34px; height:34px; border-radius:var(--r-md); background:var(--blue-soft); border:1px solid var(--blue-border); display:flex; align-items:center; justify-content:center; }
.nilai-page > *:nth-child(4) { animation-delay: 0.20s; } .toolbar-icon svg { width:16px; height:16px; color:var(--blue); }
.toolbar-badge { font-size:11.5px; font-weight:600; background:var(--surface-3); border:1px solid var(--border); color:var(--text-muted); border-radius:999px; padding:2px 10px; }
@keyframes rise { /* TABLE */
from { opacity: 0; transform: translateY(14px); } .table-responsive { overflow-x:auto; }
to { opacity: 1; transform: translateY(0); } table.nilai-table { width:100%; border-collapse:collapse; min-width:680px; }
} .nilai-table thead tr { background:var(--surface-2); border-bottom:1px solid var(--border); }
.nilai-table thead th { padding:12px 18px; font-size:11px; font-weight:700; color:var(--text-muted); text-transform:uppercase; letter-spacing:0.8px; text-align:left; white-space:nowrap; }
.nilai-table thead th.center { text-align:center; }
.nilai-table tbody tr { border-bottom:1px solid var(--border); transition:background .15s; }
.nilai-table tbody tr:last-child { border-bottom:none; }
.nilai-table tbody tr:hover { background:#f8faff; }
.nilai-table tbody td { padding:15px 18px; font-size:13.5px; color:var(--ink); vertical-align:middle; }
.nilai-table tbody td.center { text-align:center; }
/* ── HERO BANNER ──────────────────────────────────── */ .mapel-cell { display:flex; align-items:center; gap:10px; }
.page-hero { .mapel-dot { width:8px; height:8px; border-radius:50%; background:var(--blue); flex-shrink:0; }
background: var(--ink); .mapel-name { font-weight:600; color:var(--ink); }
border-radius: var(--r-2xl);
padding: 28px 32px;
margin-bottom: 24px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
position: relative;
overflow: hidden;
}
.page-hero::before { .chip { display:inline-flex; align-items:center; justify-content:center; min-width:52px; padding:5px 13px; border-radius:var(--r-md); font-family:'JetBrains Mono',monospace; font-size:13px; font-weight:700; }
content: ''; .chip.tugas { background:var(--blue-soft); color:var(--blue); border:1px solid var(--blue-border); }
position: absolute; inset: 0; .chip.uts { background:var(--amber-soft); color:var(--amber); border:1px solid var(--amber-border); }
background: .chip.uas { background:var(--green-soft); color:var(--green); border:1px solid var(--green-border); }
radial-gradient(ellipse 55% 90% at 100% 50%, rgba(37,99,235,0.22) 0%, transparent 65%), .chip.rata-a { background:var(--green-soft); color:var(--green); border:1px solid var(--green-border); }
radial-gradient(ellipse 35% 60% at 0% 0%, rgba(124,58,237,0.13) 0%, transparent 55%); .chip.rata-b { background:var(--blue-soft); color:var(--blue); border:1px solid var(--blue-border); }
pointer-events: none; .chip.rata-c { background:var(--amber-soft); color:var(--amber); border:1px solid var(--amber-border); }
} .chip.rata-d { background:var(--red-soft); color:var(--red); border:1px solid var(--red-border); }
.chip.null { background:var(--surface-3); color:var(--text-muted); border:1px solid var(--border); }
.hero-left { position: relative; z-index: 1; } .rata-wrap { display:flex; align-items:center; gap:10px; }
.mini-bar { flex:1; height:6px; background:var(--surface-3); border-radius:999px; overflow:hidden; min-width:60px; }
.mini-bar-fill { height:100%; border-radius:999px; transition:width 0.8s cubic-bezier(0.16,1,0.3,1); }
.hero-tag { /* BELUM TERSEDIA */
display: inline-flex; align-items: center; gap: 7px; .badge-pending { display:inline-flex; align-items:center; gap:5px; background:var(--surface-3); border:1px solid var(--border); border-radius:999px; padding:3px 10px; font-size:11.5px; font-weight:600; color:var(--text-muted); }
background: rgba(255,255,255,0.08);
border: 1px solid rgba(255,255,255,0.13);
border-radius: 999px;
padding: 4px 14px;
font-size: 11.5px; font-weight: 600;
color: rgba(255,255,255,0.6);
letter-spacing: 0.6px; text-transform: uppercase;
margin-bottom: 12px;
}
.hero-tag-dot {
width: 7px; height: 7px; border-radius: 50%;
background: #22c55e;
box-shadow: 0 0 7px #22c55e;
}
.hero-title {
font-size: 24px; font-weight: 800;
color: #fff; letter-spacing: -0.4px; line-height: 1.2;
}
.hero-title em { font-style: normal; color: #93c5fd; }
.hero-sub {
font-size: 13px; color: rgba(255,255,255,0.45);
margin-top: 6px;
}
.hero-right {
position: relative; z-index: 1;
display: flex; align-items: center; gap: 12px;
flex-shrink: 0;
}
.hero-stat {
background: rgba(255,255,255,0.07);
border: 1px solid rgba(255,255,255,0.11);
border-radius: var(--r-lg);
padding: 14px 20px;
text-align: center;
}
.hero-stat-val {
font-size: 28px; font-weight: 800; color: #fff;
letter-spacing: -1px; line-height: 1;
font-family: 'JetBrains Mono', monospace;
}
.hero-stat-lbl {
font-size: 11px; font-weight: 600;
color: rgba(255,255,255,0.42);
text-transform: uppercase; letter-spacing: 0.7px;
margin-top: 4px;
}
/* ── SUMMARY CARDS ────────────────────────────────── */
.summary-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 14px;
margin-bottom: 22px;
}
.sum-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--r-xl);
padding: 20px 18px;
box-shadow: var(--shadow-card);
position: relative;
overflow: hidden;
transition: box-shadow 0.2s, transform 0.2s;
}
.sum-card:hover {
box-shadow: var(--shadow-hover);
transform: translateY(-2px);
}
.sum-card::before {
content: '';
position: absolute; top: 0; left: 0; right: 0;
height: 3px; border-radius: var(--r-xl) var(--r-xl) 0 0;
}
.sum-card.blue::before { background: linear-gradient(90deg, var(--blue), #60a5fa); }
.sum-card.amber::before { background: linear-gradient(90deg, var(--amber), #fbbf24); }
.sum-card.green::before { background: linear-gradient(90deg, var(--green), #34d399); }
.sum-card.violet::before { background: linear-gradient(90deg, var(--violet), #a78bfa); }
.sum-icon {
width: 40px; height: 40px;
border-radius: var(--r-md);
display: flex; align-items: center; justify-content: center;
margin-bottom: 14px;
}
.sum-card.blue .sum-icon { background: var(--blue-soft); border: 1px solid var(--blue-border); }
.sum-card.amber .sum-icon { background: var(--amber-soft); border: 1px solid var(--amber-border); }
.sum-card.green .sum-icon { background: var(--green-soft); border: 1px solid var(--green-border); }
.sum-card.violet .sum-icon { background: var(--violet-soft); border: 1px solid var(--violet-border); }
.sum-icon svg { width: 18px; height: 18px; }
.sum-card.blue .sum-icon svg { color: var(--blue); }
.sum-card.amber .sum-icon svg { color: var(--amber); }
.sum-card.green .sum-icon svg { color: var(--green); }
.sum-card.violet .sum-icon svg { color: var(--violet); }
.sum-label {
font-size: 11px; font-weight: 700;
color: var(--text-muted);
text-transform: uppercase; letter-spacing: 0.7px;
margin-bottom: 4px;
}
.sum-value {
font-size: 26px; font-weight: 800;
color: var(--ink); letter-spacing: -0.8px;
font-family: 'JetBrains Mono', monospace;
line-height: 1;
}
.sum-sub {
font-size: 11.5px; color: var(--text-muted);
margin-top: 5px;
}
/* ── TABLE CARD ───────────────────────────────────── */
.table-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--r-xl);
box-shadow: var(--shadow-card);
overflow: hidden;
}
.table-toolbar {
display: flex; align-items: center; justify-content: space-between;
padding: 18px 24px 14px;
border-bottom: 1px solid var(--border);
flex-wrap: wrap; gap: 12px;
}
.toolbar-title {
font-size: 14px; font-weight: 700; color: var(--ink);
display: flex; align-items: center; gap: 10px;
}
.toolbar-icon {
width: 34px; height: 34px;
border-radius: var(--r-md);
background: var(--blue-soft); border: 1px solid var(--blue-border);
display: flex; align-items: center; justify-content: center;
}
.toolbar-icon svg { width: 16px; height: 16px; color: var(--blue); }
.toolbar-badge {
font-size: 11.5px; font-weight: 600;
background: var(--surface-3); border: 1px solid var(--border);
color: var(--text-muted); border-radius: 999px;
padding: 2px 10px;
}
/* ── TABLE ────────────────────────────────────────── */
.table-responsive { overflow-x: auto; }
table.nilai-table {
width: 100%;
border-collapse: collapse;
min-width: 620px;
}
.nilai-table thead tr {
background: var(--surface-2);
border-bottom: 1px solid var(--border);
}
.nilai-table thead th {
padding: 12px 18px;
font-size: 11px; font-weight: 700;
color: var(--text-muted);
text-transform: uppercase; letter-spacing: 0.8px;
text-align: left; white-space: nowrap;
}
.nilai-table thead th.center { text-align: center; }
.nilai-table tbody tr {
border-bottom: 1px solid var(--border);
transition: background 0.15s;
}
.nilai-table tbody tr:last-child { border-bottom: none; }
.nilai-table tbody tr:hover { background: #f8faff; }
.nilai-table tbody td {
padding: 15px 18px;
font-size: 13.5px; color: var(--ink);
vertical-align: middle;
}
.nilai-table tbody td.center { text-align: center; }
/* NO CELL */
.cell-no {
font-family: 'JetBrains Mono', monospace;
font-size: 12.5px; color: var(--text-muted);
font-weight: 500; width: 50px;
}
/* MAPEL CELL */
.mapel-cell {
display: flex; align-items: center; gap: 10px;
}
.mapel-dot {
width: 8px; height: 8px; border-radius: 50%;
background: var(--blue); flex-shrink: 0;
}
.mapel-name {
font-weight: 600; color: var(--ink);
}
/* NILAI CHIPS */
.chip {
display: inline-flex; align-items: center; justify-content: center;
min-width: 52px;
padding: 5px 13px;
border-radius: var(--r-md);
font-family: 'JetBrains Mono', monospace;
font-size: 13px; font-weight: 700;
}
.chip.tugas { background: var(--blue-soft); color: var(--blue); border: 1px solid var(--blue-border); }
.chip.uts { background: var(--amber-soft); color: var(--amber); border: 1px solid var(--amber-border); }
.chip.uas { background: var(--green-soft); color: var(--green); border: 1px solid var(--green-border); }
/* RATA CHIP — dynamic color */
.chip.rata-a { background: var(--green-soft); color: var(--green); border: 1px solid var(--green-border); }
.chip.rata-b { background: var(--blue-soft); color: var(--blue); border: 1px solid var(--blue-border); }
.chip.rata-c { background: var(--amber-soft); color: var(--amber); border: 1px solid var(--amber-border); }
.chip.rata-d { background: var(--red-soft); color: var(--red); border: 1px solid var(--red-border); }
/* PROGRESS BAR inline */
.rata-wrap {
display: flex; align-items: center; gap: 10px;
}
.mini-bar {
flex: 1; height: 6px;
background: var(--surface-3);
border-radius: 999px;
overflow: hidden;
min-width: 60px;
}
.mini-bar-fill {
height: 100%; border-radius: 999px;
transition: width 0.8s cubic-bezier(0.16,1,0.3,1);
}
/* EMPTY */ /* EMPTY */
.empty-state { .empty-state { padding:64px 24px; text-align:center; }
padding: 64px 24px; text-align: center; .empty-icon { width:60px; height:60px; background:var(--surface-2); border:1px solid var(--border); border-radius:50%; display:flex; align-items:center; justify-content:center; margin:0 auto 14px; }
} .empty-icon svg { width:26px; height:26px; color:var(--text-muted); }
.empty-title { font-size:15px; font-weight:700; color:var(--ink); margin-bottom:6px; }
.empty-desc { font-size:13px; color:var(--text-muted); }
.empty-icon { .table-footer { display:flex; align-items:center; justify-content:space-between; padding:13px 24px; border-top:1px solid var(--border); background:var(--surface-2); font-size:12.5px; color:var(--text-muted); flex-wrap:wrap; gap:6px; }
width: 60px; height: 60px;
background: var(--surface-2); border: 1px solid var(--border);
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
margin: 0 auto 14px;
}
.empty-icon svg { width: 26px; height: 26px; color: var(--text-muted); } @media(max-width:1024px){ .summary-grid{grid-template-columns:repeat(2,1fr);} }
.empty-title { font-size: 15px; font-weight: 700; color: var(--ink); margin-bottom: 6px; } @media(max-width:768px){ .nilai-page{padding:16px 16px 48px;} .hero-right{display:none;} .summary-grid{grid-template-columns:repeat(2,1fr);gap:10px;} }
.empty-desc { font-size: 13px; color: var(--text-muted); }
/* TABLE FOOTER */
.table-footer {
display: flex; align-items: center; justify-content: space-between;
padding: 13px 24px;
border-top: 1px solid var(--border);
background: var(--surface-2);
font-size: 12.5px; color: var(--text-muted);
flex-wrap: wrap; gap: 6px;
}
/* ── RESPONSIVE ───────────────────────────────────── */
@media (max-width: 1024px) { .summary-grid { grid-template-columns: repeat(2,1fr); } }
@media (max-width: 768px) {
.nilai-page { padding: 16px 16px 48px; }
.page-hero { padding: 22px 20px; }
.hero-title { font-size: 20px; }
.hero-right { display: none; }
.summary-grid { grid-template-columns: repeat(2,1fr); gap: 10px; }
}
@media (max-width: 420px) {
.summary-grid { grid-template-columns: 1fr 1fr; }
}
</style> </style>
<div class="nilai-page"> <div class="nilai-page">
{{-- ── HERO ──────────────────────────────────────── --}} {{-- HERO --}}
<div class="page-hero"> <div class="page-hero">
<div class="hero-left"> <div class="hero-left">
<div class="hero-tag"> <div class="hero-tag"><span class="hero-tag-dot"></span>Rekap Akademik</div>
<span class="hero-tag-dot"></span> <h1 class="hero-title">Data <em>Nilai Saya</em></h1>
Rekap Akademik <p class="hero-sub">Nilai tugas, UTS, UAS, dan rata-rata per mata pelajaran yang dikirim guru</p>
</div>
<h1 class="hero-title">Data <em>Nilai Siswa</em></h1>
<p class="hero-sub">Ringkasan nilai tugas, UTS, UAS, dan rata-rata per mata pelajaran</p>
</div> </div>
<div class="hero-right"> <div class="hero-right">
<div class="hero-stat">
<div class="hero-stat-val">{{ count($data) }}</div>
<div class="hero-stat-lbl">Mata Pelajaran</div>
</div>
@php @php
$avgAll = collect($data)->avg('rata'); $mapelCount = count($data);
$avgAll = collect($data)->avg('rata');
@endphp @endphp
<div class="hero-stat"> <div class="hero-stat">
<div class="hero-stat-val">{{ $avgAll ? number_format($avgAll, 1) : '—' }}</div> <div class="hero-stat-val">{{ $mapelCount }}</div>
<div class="hero-stat-lbl">Mata Pelajaran</div>
</div>
<div class="hero-stat">
<div class="hero-stat-val">{{ $avgAll ? number_format($avgAll,1) : '—' }}</div>
<div class="hero-stat-lbl">Rata-rata</div> <div class="hero-stat-lbl">Rata-rata</div>
</div> </div>
</div> </div>
</div> </div>
{{-- ── SUMMARY CARDS ──────────────────────────────── --}} {{-- SUMMARY --}}
@php @php
$avgTugas = collect($data)->whereNotNull('tugas')->avg('tugas'); $avgTugas = collect($data)->where('tugas', '!=', null)->avg('tugas');
$avgUts = collect($data)->whereNotNull('uts')->avg('uts'); $avgUts = collect($data)->where('uts', '!=', null)->avg('uts');
$avgUas = collect($data)->whereNotNull('uas')->avg('uas'); $avgUas = collect($data)->where('uas', '!=', null)->avg('uas');
$avgRata = collect($data)->whereNotNull('rata')->avg('rata'); $avgRata = collect($data)->where('rata', '!=', null)->avg('rata');
@endphp @endphp
<div class="summary-grid"> <div class="summary-grid">
<div class="sum-card blue"> <div class="sum-card blue">
<div class="sum-icon"> <div class="sum-icon"><svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/></svg></div>
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/>
</svg>
</div>
<div class="sum-label">Avg. Tugas</div> <div class="sum-label">Avg. Tugas</div>
<div class="sum-value">{{ $avgTugas ? number_format($avgTugas,1) : '—' }}</div> <div class="sum-value">{{ $avgTugas ? number_format($avgTugas,1) : '—' }}</div>
<div class="sum-sub">rata-rata nilai tugas</div> <div class="sum-sub">rata-rata nilai tugas</div>
</div> </div>
<div class="sum-card amber"> <div class="sum-card amber">
<div class="sum-icon"> <div class="sum-icon"><svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg></div>
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
</div>
<div class="sum-label">Avg. UTS</div> <div class="sum-label">Avg. UTS</div>
<div class="sum-value">{{ $avgUts ? number_format($avgUts,1) : '—' }}</div> <div class="sum-value">{{ $avgUts ? number_format($avgUts,1) : '—' }}</div>
<div class="sum-sub">rata-rata nilai UTS</div> <div class="sum-sub">rata-rata nilai UTS</div>
</div> </div>
<div class="sum-card green"> <div class="sum-card green">
<div class="sum-icon"> <div class="sum-icon"><svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z"/></svg></div>
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z"/>
</svg>
</div>
<div class="sum-label">Avg. UAS</div> <div class="sum-label">Avg. UAS</div>
<div class="sum-value">{{ $avgUas ? number_format($avgUas,1) : '—' }}</div> <div class="sum-value">{{ $avgUas ? number_format($avgUas,1) : '—' }}</div>
<div class="sum-sub">rata-rata nilai UAS</div> <div class="sum-sub">rata-rata nilai UAS</div>
</div> </div>
<div class="sum-card violet"> <div class="sum-card violet">
<div class="sum-icon"> <div class="sum-icon"><svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/></svg></div>
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
</svg>
</div>
<div class="sum-label">Avg. Keseluruhan</div> <div class="sum-label">Avg. Keseluruhan</div>
<div class="sum-value">{{ $avgRata ? number_format($avgRata,1) : '—' }}</div> <div class="sum-value">{{ $avgRata ? number_format($avgRata,1) : '—' }}</div>
<div class="sum-sub">rata-rata semua mapel</div> <div class="sum-sub">rata-rata semua mapel</div>
</div> </div>
</div> </div>
{{-- ── TABLE ──────────────────────────────────────── --}} {{-- TABLE --}}
<div class="table-card"> <div class="table-card">
<div class="table-toolbar"> <div class="table-toolbar">
<div class="toolbar-title"> <div class="toolbar-title">
<div class="toolbar-icon"> <div class="toolbar-icon"><svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/></svg></div>
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
</svg>
</div>
Rekap Nilai Per Mapel Rekap Nilai Per Mapel
<span class="toolbar-badge">{{ count($data) }} mapel</span> <span class="toolbar-badge">{{ count($data) }} mapel</span>
</div> </div>
@ -507,48 +201,54 @@
@forelse($data as $d) @forelse($data as $d)
@php @php
$rata = $d['rata'] ?? null; $rata = $d['rata'] ?? null;
$rataClass = !$rata ? 'rata-b' $rataClass = !$rata ? 'null'
: ($rata >= 85 ? 'rata-a' : ($rata >= 85 ? 'rata-a'
: ($rata >= 70 ? 'rata-b' : ($rata >= 70 ? 'rata-b'
: ($rata >= 55 ? 'rata-c' : 'rata-d'))); : ($rata >= 55 ? 'rata-c' : 'rata-d')));
$barColor = !$rata ? '#2563eb' $barColor = !$rata ? '#2563eb'
: ($rata >= 85 ? '#059669' : ($rata >= 85 ? '#059669'
: ($rata >= 70 ? '#2563eb' : ($rata >= 70 ? '#2563eb'
: ($rata >= 55 ? '#d97706' : '#dc2626'))); : ($rata >= 55 ? '#d97706' : '#dc2626')));
$pct = $rata ? min($rata, 100) : 0; $pct = $rata ? min($rata, 100) : 0;
@endphp @endphp
<tr> <tr>
<td class="cell-no">{{ $loop->iteration }}</td> <td style="font-family:'JetBrains Mono',monospace;font-size:12.5px;color:var(--text-muted);">{{ $loop->iteration }}</td>
<td> <td>
<div class="mapel-cell"> <div class="mapel-cell">
<div class="mapel-dot" style="background:{{ $barColor }};"></div> <div class="mapel-dot" style="background:{{ $barColor }};"></div>
<span class="mapel-name">{{ $d['mapel'] ?? '—' }}</span> <span class="mapel-name">{{ $d['mapel'] ?? '—' }}</span>
</div> </div>
</td> </td>
<td class="center"> <td class="center">
<span class="chip tugas">{{ $d['tugas'] ?? '—' }}</span> @if(isset($d['tugas']) && $d['tugas'] !== null)
<span class="chip tugas">{{ $d['tugas'] }}</span>
@else
<span class="badge-pending">Belum</span>
@endif
</td> </td>
<td class="center"> <td class="center">
<span class="chip uts">{{ $d['uts'] ?? '—' }}</span> @if(isset($d['uts']) && $d['uts'] !== null)
<span class="chip uts">{{ $d['uts'] }}</span>
@else
<span class="badge-pending">Belum</span>
@endif
</td> </td>
<td class="center"> <td class="center">
<span class="chip uas">{{ $d['uas'] ?? '—' }}</span> @if(isset($d['uas']) && $d['uas'] !== null)
<span class="chip uas">{{ $d['uas'] }}</span>
@else
<span class="badge-pending">Belum</span>
@endif
</td> </td>
<td> <td>
<div class="rata-wrap"> <div class="rata-wrap">
<span class="chip {{ $rataClass }}">{{ $rata ?? '—' }}</span>
@if($rata) @if($rata)
<div class="mini-bar"> <span class="chip {{ $rataClass }}">{{ $rata }}</span>
<div class="mini-bar-fill" <div class="mini-bar">
style="width:{{ $pct }}%; background:{{ $barColor }};"></div> <div class="mini-bar-fill" style="width:{{ $pct }}%;background:{{ $barColor }};"></div>
</div> </div>
@else
<span class="badge-pending">Menunggu guru</span>
@endif @endif
</div> </div>
</td> </td>
@ -557,13 +257,9 @@
<tr> <tr>
<td colspan="6"> <td colspan="6">
<div class="empty-state"> <div class="empty-state">
<div class="empty-icon"> <div class="empty-icon"><svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/></svg></div>
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
</svg>
</div>
<div class="empty-title">Belum Ada Data Nilai</div> <div class="empty-title">Belum Ada Data Nilai</div>
<div class="empty-desc">Nilai akan muncul setelah guru menginput data penilaian.</div> <div class="empty-desc">Nilai akan muncul setelah guru mengirimkan nilai ke kamu.</div>
</div> </div>
</td> </td>
</tr> </tr>
@ -574,12 +270,11 @@
@if(count($data) > 0) @if(count($data) > 0)
<div class="table-footer"> <div class="table-footer">
<span>Menampilkan {{ count($data) }} mata pelajaran</span> <span>{{ count($data) }} mata pelajaran</span>
<span>{{ now()->format('d F Y') }}</span> <span>{{ now()->format('d F Y') }}</span>
</div> </div>
@endif @endif
</div> </div>
</div> </div>
@endsection @endsection

View File

@ -429,7 +429,7 @@
<th class="center">Aksi</th> <th class="center">Aksi</th>
</tr> </tr>
</thead> </thead>
<tbody> <!-- <tbody>
@forelse($tugas as $t) @forelse($tugas as $t)
<tr> <tr>
<td><span class="judul-cell">{{ $t->judul }}</span></td> <td><span class="judul-cell">{{ $t->judul }}</span></td>
@ -516,7 +516,7 @@
</td> </td>
</tr> </tr>
@endforelse @endforelse
</tbody> </tbody> -->
</table> </table>
</div> </div>

View File

@ -66,9 +66,7 @@
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| OTP LOGIN (Langkah 1 & 2 di halaman login) | OTP LOGIN
| sendOtp : kirim OTP ke email SAJA atau WhatsApp SAJA (tidak keduanya)
| loginOtp : login dengan email + password + kode OTP
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
*/ */
Route::post('/send-otp', [OtpController::class, 'sendOtp'])->name('send.otp'); Route::post('/send-otp', [OtpController::class, 'sendOtp'])->name('send.otp');
@ -76,7 +74,7 @@
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| FORCE PASSWORD (ganti password default setelah login pertama) | FORCE PASSWORD
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
*/ */
Route::middleware('auth')->group(function () { Route::middleware('auth')->group(function () {
@ -90,10 +88,9 @@
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| LUPA PASSWORD via OTP (flow: kirim OTP verify reset) | LUPA PASSWORD
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
*/ */
// Langkah 1: Form kirim OTP lupa password
Route::get('/forgot-password', function () { Route::get('/forgot-password', function () {
return view('auth.forgot-password'); return view('auth.forgot-password');
})->name('password.request'); })->name('password.request');
@ -101,14 +98,12 @@
Route::post('/forgot-password', [AuthController::class, 'sendOtp']) Route::post('/forgot-password', [AuthController::class, 'sendOtp'])
->name('password.email'); ->name('password.email');
// Langkah 2: Verify OTP
Route::get('/verify-otp', [AuthController::class, 'showVerifyOtp']) Route::get('/verify-otp', [AuthController::class, 'showVerifyOtp'])
->name('verify.otp.form'); ->name('verify.otp.form');
Route::post('/verify-otp', [AuthController::class, 'verifyOtp']) Route::post('/verify-otp', [AuthController::class, 'verifyOtp'])
->name('verify.otp'); ->name('verify.otp');
// Langkah 3: Reset password baru
Route::get('/reset-password', [AuthController::class, 'showResetPassword']) Route::get('/reset-password', [AuthController::class, 'showResetPassword'])
->name('reset.password.form'); ->name('reset.password.form');
@ -145,7 +140,7 @@
Route::post('/siswa/bulk/nonaktif', [SiswaController::class, 'bulkNonaktif'])->name('siswa.bulkNonaktif'); Route::post('/siswa/bulk/nonaktif', [SiswaController::class, 'bulkNonaktif'])->name('siswa.bulkNonaktif');
}); });
// Siswa nonaktif/aktifkan (di luar middleware force.password agar fleksibel) // Siswa nonaktif/aktifkan
Route::prefix('admin')->name('admin.')->group(function () { Route::prefix('admin')->name('admin.')->group(function () {
Route::post('/siswa/nonaktif/{id}', [SiswaController::class, 'nonaktif'])->name('siswa.nonaktif'); Route::post('/siswa/nonaktif/{id}', [SiswaController::class, 'nonaktif'])->name('siswa.nonaktif');
Route::get('/siswa/aktifkan/{id}', [SiswaController::class, 'aktifkan'])->name('siswa.aktifkan'); Route::get('/siswa/aktifkan/{id}', [SiswaController::class, 'aktifkan'])->name('siswa.aktifkan');
@ -172,12 +167,19 @@
Route::put('/absensi/update/{id}', [AbsensiController::class, 'update'])->name('absensi.update'); Route::put('/absensi/update/{id}', [AbsensiController::class, 'update'])->name('absensi.update');
// NILAI // NILAI
// CATATAN: route dengan path statis (tanpa parameter) didaftarkan lebih dulu
// agar Laravel tidak salah parsing saat ada route dengan {id}
Route::get('/nilai', [GuruNilai::class, 'index'])->name('nilai.index'); Route::get('/nilai', [GuruNilai::class, 'index'])->name('nilai.index');
Route::get('/nilai/input', [GuruNilai::class, 'inputNilai'])->name('nilai.input'); Route::get('/nilai/input', [GuruNilai::class, 'inputNilai'])->name('nilai.input');
Route::get('/get-siswa/{id}', [GuruNilai::class, 'getSiswaByKelas'])->name('get.siswa'); Route::get('/get-siswa/{id}', [GuruNilai::class, 'getSiswaByKelas'])->name('get.siswa');
Route::post('/nilai/store-batch', [GuruNilai::class, 'storeBatch'])->name('nilai.storeBatch'); Route::post('/nilai/store-batch', [GuruNilai::class, 'storeBatch'])->name('nilai.storeBatch');
Route::post('/nilai/masukkan-tugas', [GuruNilai::class, 'masukkanNilaiTugas'])->name('nilai.masukkanTugas');
Route::post('/nilai/masukkan-ujian', [GuruNilai::class, 'masukkanNilaiUjian'])->name('nilai.masukkanUjian');
Route::post('/nilai/hitung-rata', [GuruNilai::class, 'hitungRata'])->name('nilai.hitungRata');
Route::post('/nilai/kirim-ke-siswa', [GuruNilai::class, 'kirimKeSiswa'])->name('nilai.kirimKeSiswa');
Route::get('/nilai/{id}/edit', [GuruNilai::class, 'edit'])->name('nilai.edit'); Route::get('/nilai/{id}/edit', [GuruNilai::class, 'edit'])->name('nilai.edit');
Route::put('/nilai/{id}', [GuruNilai::class, 'update'])->name('nilai.update'); Route::put('/nilai/{id}', [GuruNilai::class, 'update'])->name('nilai.update');
Route::delete('/nilai/{id}', [GuruNilai::class, 'destroy'])->name('nilai.destroy');
// TUGAS // TUGAS
Route::resource('tugas', TugasController::class); Route::resource('tugas', TugasController::class);
@ -197,27 +199,9 @@
Route::delete('/ujian/{id}', [GuruUjian::class, 'destroy'])->name('ujian.destroy'); Route::delete('/ujian/{id}', [GuruUjian::class, 'destroy'])->name('ujian.destroy');
Route::get('/ujian/{id}/soal', [GuruUjian::class, 'soal'])->name('ujian.soal'); Route::get('/ujian/{id}/soal', [GuruUjian::class, 'soal'])->name('ujian.soal');
Route::post('/ujian/{id}/soal', [GuruUjian::class, 'storeSoal'])->name('ujian.storeSoal'); Route::post('/ujian/{id}/soal', [GuruUjian::class, 'storeSoal'])->name('ujian.storeSoal');
Route::post('/ujian/{id}/kirim', [GuruUjian::class, 'kirim'])->name('ujian.kirim');
}); });
// routes/web.php — tambahkan di dalam group guru
// di dalam group guru
// UJIAN
Route::get('/ujian', [GuruUjian::class, 'index'])->name('ujian.index');
Route::get('/ujian/create', [GuruUjian::class, 'create'])->name('ujian.create');
Route::post('/ujian/store', [GuruUjian::class, 'store'])->name('ujian.store');
Route::get('/ujian/{id}/edit', [GuruUjian::class, 'edit'])->name('ujian.edit');
Route::put('/ujian/{id}', [GuruUjian::class, 'update'])->name('ujian.update');
Route::delete('/ujian/{id}', [GuruUjian::class, 'destroy'])->name('ujian.destroy');
Route::get('/ujian/{id}/soal', [GuruUjian::class, 'soal'])->name('ujian.soal');
Route::post('/ujian/{id}/soal', [GuruUjian::class, 'storeSoal'])->name('ujian.storeSoal');
Route::post('/ujian/{id}/kirim', [GuruUjian::class, 'kirim'])->name('ujian.kirim'); // ✅ TAMBAH DI SINI
// Guru: cek status middleware tambahan (jika diperlukan)
Route::middleware(['auth', 'cek.status'])->prefix('guru')->name('guru.')->group(function () {
// tambahkan route khusus cek.status di sini jika ada
});
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| SISWA | SISWA