flow admin-guru

This commit is contained in:
RetasyaSalsabila 2026-03-08 12:49:06 +07:00
parent e0db94bf1a
commit ce2ca560f9
10 changed files with 914 additions and 271 deletions

View File

@ -4,121 +4,153 @@
use App\Http\Controllers\Controller;
use App\Models\Guru;
use App\Models\Mapel;
use App\Models\Kelas;
use App\Models\Mengajar;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
class GuruController extends Controller
{
public function index(Request $request)
{
$query = Guru::query();
{
$query = Guru::with('mengajars.mapel', 'mengajars.kelas');
if ($request->filled('search')) {
$search = $request->search;
$query->where(function($q) use ($search) {
$q->where('nama', 'like', "%$search%")
->orWhere('nip', 'like', "%$search%");
});
}
// SEARCH
if ($request->has('search')) {
$search = $request->search;
$query->where('nama', 'like', "%$search%")
->orWhere('nip', 'like', "%$search%");
$perPage = $request->get('perPage', 10);
$gurus = $query->paginate($perPage)->appends($request->all());
$mapels = Mapel::all();
$kelas = Kelas::all();
return view('admin.guru.index', compact('gurus', 'mapels', 'kelas'));
}
// SHOW PER PAGE
$perPage = $request->get('perPage', 10);
$gurus = $query->paginate($perPage)->appends($request->all());
return view('admin.guru.index', compact('gurus'));
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
return view('admin.guru.create');
}
/**
* Store a newly created resource in storage.
* Tambah guru + bisa pilih banyak pasangan (mapel, kelas)
* Input: mapel[] dan kelas[] dengan index yang bersesuaian
*/
public function store(Request $request)
{
$validated = $request->validate([
'nip' => 'required|string|max:30|unique:gurus,nip',
'nama' => 'required|string|max:100',
'password' => 'required|string|min:6',
$request->validate([
'nip' => 'required|string|max:30|unique:gurus,nip',
'nama' => 'required|string|max:100',
'password' => 'required|string|min:6',
'id_mapel' => 'required|array|min:1',
'id_mapel.*' => 'required|exists:mapels,id_mapel',
'id_kelas' => 'required|array|min:1',
'id_kelas.*' => 'required|exists:kelas,id_kelas',
], [
'nip.required' => 'NIP wajib diisi',
'nip.unique' => 'NIP sudah terdaftar',
'nama.required' => 'Nama wajib diisi',
'password.required' => 'Password wajib diisi',
'password.min' => 'Password minimal 6 karakter',
'id_mapel.required' => 'Pilih minimal 1 mata pelajaran.',
'id_kelas.required' => 'Pilih minimal 1 kelas.',
]);
Guru::create([
'nip' => $validated['nip'],
'nama' => $validated['nama'],
'password' => Hash::make($validated['password']),
$guru = Guru::create([
'nip' => $request->nip,
'nama' => $request->nama,
'password' => Hash::make($request->password),
]);
// Tiap mapel berpasangan dengan kelas di index yang sama
foreach ($request->id_mapel as $i => $idMapel) {
$idKelas = $request->id_kelas[$i] ?? $request->id_kelas[0];
Mengajar::create([
'id_guru' => $guru->id_guru,
'id_mapel' => $idMapel,
'id_kelas' => $idKelas,
]);
}
return redirect()->route('admin.guru.index')
->with('success', 'Data guru berhasil ditambahkan!');
->with('success', 'Data guru berhasil ditambahkan.');
}
/**
* Display the specified resource.
*/
public function show(string $nip)
public function show(string $id)
{
$guru = Guru::findOrFail($nip);
$guru = Guru::findOrFail($id);
return view('admin.guru.show', compact('guru'));
}
/**
* Show the form for editing the specified resource.
*/
public function edit(string $nip)
public function edit(string $id)
{
$guru = Guru::findOrFail($nip);
$guru = Guru::findOrFail($id);
return view('admin.guru.edit', compact('guru'));
}
/**
* Update the specified resource in storage.
* Update guru hapus semua mengajar lama, insert ulang
*/
public function update(Request $request, string $nip)
public function update(Request $request, string $id)
{
$guru = Guru::findOrFail($nip);
$guru = Guru::findOrFail($id);
$validated = $request->validate([
'nama' => 'required|string|max:100',
'password' => 'nullable|string|min:6',
], [
'nama.required' => 'Nama wajib diisi',
'password.min' => 'Password minimal 6 karakter',
$request->validate([
'nip' => 'required|string|max:30|unique:gurus,nip,' . $guru->id_guru . ',id_guru',
'nama' => 'required|string|max:100',
'password' => 'nullable|string|min:6',
'id_mapel' => 'required|array|min:1',
'id_mapel.*' => 'required|exists:mapels,id_mapel',
'id_kelas' => 'required|array|min:1',
'id_kelas.*' => 'required|exists:kelas,id_kelas',
]);
$guru->nama = $validated['nama'];
// Update password hanya jika diisi
$guru->nip = $request->nip;
$guru->nama = $request->nama;
if ($request->filled('password')) {
$guru->password = Hash::make($validated['password']);
$guru->password = Hash::make($request->password);
}
$guru->save();
// Hapus semua data mengajar lama, insert ulang
Mengajar::where('id_guru', $guru->id_guru)->delete();
foreach ($request->id_mapel as $i => $idMapel) {
$idKelas = $request->id_kelas[$i] ?? $request->id_kelas[0];
Mengajar::create([
'id_guru' => $guru->id_guru,
'id_mapel' => $idMapel,
'id_kelas' => $idKelas,
]);
}
return redirect()->route('admin.guru.index')
->with('success', 'Data guru berhasil diupdate!');
->with('success', 'Data guru berhasil diupdate.');
}
/**
* Remove the specified resource from storage.
*/
public function destroy(string $nip)
public function destroy(string $id)
{
$guru = Guru::findOrFail($nip);
$guru = Guru::findOrFail($id);
$guru->delete();
return redirect()->route('admin.guru.index')
->with('success', 'Data guru berhasil dihapus!');
->with('success', 'Data guru berhasil dihapus.');
}
/**
* API: Ambil kelas yang memiliki mapel tertentu (lewat tabel mengajars)
* Dipanggil via AJAX saat admin pilih mapel di modal
*/
public function getKelasByMapel(Request $request)
{
$idMapel = $request->id_mapel;
// Cari kelas yang sudah punya mapel ini di tabel mengajars
$kelasList = Kelas::whereHas('mengajars', function ($q) use ($idMapel) {
$q->where('id_mapel', $idMapel);
})
->get(['id_kelas', 'nama_kelas', 'tingkat']);
return response()->json($kelasList);
}
}

View File

@ -0,0 +1,62 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Mengajar;
use App\Models\Guru;
use App\Models\Mapel;
use App\Models\Kelas;
use Illuminate\Http\Request;
class MengajarController extends Controller
{
public function index()
{
$mengajars = Mengajar::with('guru','mapel','kelas')->get();
$gurus = Guru::all();
$mapels = Mapel::all();
$kelas = Kelas::all();
return view('admin.mengajar.index', compact(
'mengajars','gurus','mapels','kelas'
));
}
public function store(Request $request)
{
$request->validate([
'id_guru' => 'required',
'id_mapel' => 'required',
'id_kelas' => 'required',
]);
Mengajar::create([
'id_guru' => $request->id_guru,
'id_mapel' => $request->id_mapel,
'id_kelas' => $request->id_kelas,
]);
return redirect()->back()->with('success','Data mengajar berhasil ditambahkan');
}
public function update(Request $request, $id)
{
$mengajar = Mengajar::findOrFail($id);
$mengajar->update([
'id_guru' => $request->id_guru,
'id_mapel' => $request->id_mapel,
'id_kelas' => $request->id_kelas,
]);
return redirect()->back()->with('success','Data mengajar berhasil diupdate');
}
public function destroy($id)
{
Mengajar::findOrFail($id)->delete();
return redirect()->back()->with('success','Data mengajar berhasil dihapus');
}
}

View File

@ -4,8 +4,6 @@
use App\Http\Controllers\Controller;
use App\Models\Mengajar;
use App\Models\Guru;
use App\Models\Kelas;
use App\Models\Siswa;
use Illuminate\Support\Facades\Auth;
@ -14,28 +12,23 @@ class DashboardController extends Controller
public function index()
{
$guru = Auth::guard('guru')->user();
// Cek table mengajars ada data atau enggak
try {
// Hitung total kelas yang diajar
$totalKelas = Mengajar::where('nip', $guru->nip)
$totalKelas = Mengajar::where('id_guru', $guru->id_guru)
->distinct('id_kelas')
->count('id_kelas');
// Hitung total mapel yang diajar
$totalMapel = Mengajar::where('nip', $guru->nip)
$totalMapel = Mengajar::where('id_guru', $guru->id_guru)
->distinct('id_mapel')
->count('id_mapel');
// Hitung total siswa yang diajar (lewat kelas)
$kelasIds = Mengajar::where('nip', $guru->nip)
$kelasIds = Mengajar::where('id_guru', $guru->id_guru)
->pluck('id_kelas')
->unique();
$totalSiswa = Siswa::whereIn('id_kelas', $kelasIds)->count();
} catch (\Exception $e) {
// Kalau error (table kosong atau relasi belum ada), set default 0
$totalKelas = 0;
$totalMapel = 0;
$totalSiswa = 0;

View File

@ -4,6 +4,10 @@
use App\Http\Controllers\Controller;
use App\Models\Mapel;
use App\Models\Materi;
use App\Models\Tugas;
use App\Models\Mengajar;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class MapelController extends Controller
@ -12,11 +16,85 @@ public function index()
{
$guru = Auth::guard('guru')->user();
// Ambil hanya mapel yang dia ajar
$mapels = Mapel::whereHas('mengajars', function ($query) use ($guru) {
$query->where('nip', $guru->nip);
})->paginate(10);
// Ambil mengajar dengan relasi mapel & kelas
// Group by id_mapel agar tidak duplikat jika guru ajar mapel sama di kelas berbeda
$mengajars = Mengajar::with(['mapel', 'kelas'])
->where('id_guru', $guru->id_guru)
->get()
->groupBy('id_mapel'); // group by mapel
return view('guru.mapel.index', compact('mapels'));
return view('guru.mapel.index', compact('mengajars'));
}
}
/**
* Simpan materi baru
*/
public function storeMateri(Request $request)
{
$guru = Auth::guard('guru')->user();
$request->validate([
'id_mengajar' => 'required|exists:mengajars,id_mengajar',
'judul_materi' => 'required|string|max:200',
'deskripsi' => 'nullable|string',
'lampiran_materi' => 'nullable|file|mimes:pdf,doc,docx,jpg,jpeg,png,ppt,pptx|max:10240',
], [
'lampiran_materi.mimes' => 'Format file: pdf, doc, docx, jpg, png, ppt, pptx.',
'lampiran_materi.max' => 'Ukuran file maksimal 10MB.',
]);
// Pastikan mengajar ini milik guru yang login
$mengajar = Mengajar::where('id_mengajar', $request->id_mengajar)
->where('id_guru', $guru->id_guru)
->firstOrFail();
$path = null;
if ($request->hasFile('lampiran_materi')) {
$file = $request->file('lampiran_materi');
$filename = 'materi_' . $guru->id_guru . '_' . time() . '.' . $file->getClientOriginalExtension();
$path = $file->storeAs('materi', $filename, 'public');
}
Materi::create([
'id_mengajar' => $request->id_mengajar,
'judul_materi' => $request->judul_materi,
'deskripsi' => $request->deskripsi,
'lampiran_materi' => $path,
]);
return redirect()->route('guru.mapel.index')
->with('success', 'Materi berhasil diupload!');
}
/**
* Simpan tugas baru
*/
public function storeTugas(Request $request)
{
$guru = Auth::guard('guru')->user();
$request->validate([
'id_mengajar' => 'required|exists:mengajars,id_mengajar',
'judul_tugas' => 'required|string|max:200',
'keterangan' => 'nullable|string',
'deadline' => 'required|date|after:now',
], [
'deadline.after' => 'Deadline harus lebih dari waktu sekarang.',
]);
// Pastikan mengajar ini milik guru yang login
$mengajar = Mengajar::where('id_mengajar', $request->id_mengajar)
->where('id_guru', $guru->id_guru)
->firstOrFail();
Tugas::create([
'id_mengajar' => $request->id_mengajar,
'judul_tugas' => $request->judul_tugas,
'keterangan' => $request->keterangan,
'deadline' => $request->deadline,
]);
return redirect()->route('guru.mapel.index')
->with('success', 'Tugas berhasil dibuat!');
}
}

View File

@ -27,6 +27,6 @@ class Guru extends Authenticatable
// Relasi ke Mengajar
public function mengajars()
{
return $this->hasMany(Mengajar::class, 'nip', 'nip');
return $this->hasMany(Mengajar::class, 'id_guru', 'id_guru');
}
}

View File

@ -30,11 +30,10 @@
gap: 8px;
font-size: 14px;
text-decoration: none;
cursor: pointer;
}
.table-header {
background: #a5e6ba;
}
.table-header { background: #a5e6ba; }
.search-box {
background: #a5e6ba;
@ -52,11 +51,7 @@
width: 150px;
}
.action-icon {
width: 20px;
cursor: pointer;
margin: 0 5px;
}
.action-icon { width: 20px; cursor: pointer; margin: 0 5px; }
.per-page-select {
border-radius: 10px;
@ -64,8 +59,6 @@
border: 1px solid #ccc;
}
/* ===== STYLE BARU UNTUK MODAL ===== */
.modal-header-pastel {
background: #FFD97D !important;
color: black !important;
@ -73,57 +66,101 @@
}
.modal-content {
border-radius: 15px;
border-radius: 20px;
border: none;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
}
.modal-body label {
font-weight: bold;
.modal-body label { font-weight: bold; }
/* ===== PASANGAN MAPEL - KELAS ===== */
.mengajar-row {
display: flex;
gap: 10px;
align-items: center;
background: #f8fafc;
border-radius: 10px;
padding: 10px 12px;
margin-bottom: 8px;
border: 1px solid #e2e8f0;
}
.modal {
left: auto !important;
right: 0;
width: calc(100% - 250px);
}
.mengajar-row select {
flex: 1;
border-radius: 8px;
border: 1px solid #cbd5e1;
padding: 6px 10px;
font-size: 14px;
}
.modal-backdrop {
left: auto !important;
right: 0;
width: calc(100% - 250px);
}
.btn-hapus-row {
background: #fee2e2;
color: #ef4444;
border: none;
border-radius: 8px;
width: 32px;
height: 32px;
font-size: 16px;
cursor: pointer;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s;
}
.modal-dialog {
margin-right: auto;
margin-left: auto;
}
.btn-hapus-row:hover { background: #fca5a5; }
.modal-content {
border-radius: 20px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
}
.btn-tambah-row {
background: #e6f0ff;
color: #2b8ef3;
border: none;
border-radius: 8px;
padding: 7px 14px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
margin-top: 4px;
transition: background 0.2s;
}
.btn-tambah-row:hover { background: #bfdbfe; }
.row-label {
font-size: 12px;
color: #64748b;
font-weight: 600;
min-width: 50px;
}
.alert-success-custom {
background: #dcfce7;
color: #166534;
border-radius: 10px;
padding: 12px 16px;
margin-bottom: 16px;
font-weight: 500;
}
</style>
<h3 class="page-title">DAFTAR GURU</h3>
@if(session('success'))
<div class="alert-success-custom"> {{ session('success') }}</div>
@endif
<div class="custom-card">
<div class="d-flex justify-content-between align-items-center mb-3">
<div class="d-flex gap-2">
<button class="btn-primary-custom" data-bs-toggle="modal" data-bs-target="#modalTambah">
<img src="{{ asset('images/icon/main/add.png') }}" width="18">
Tambah Data
</button>
<button class="btn-primary-custom">
<img src="{{ asset('images/icon/main/download.png') }}" width="18">
Download PDF
</button>
<button class="btn-primary-custom">
<img src="{{ asset('images/icon/main/download.png') }}" width="18">
Download Excel
@ -138,21 +175,17 @@
</button>
</div>
</form>
</div>
<form method="GET" class="mb-2">
<span>Tampilkan</span>
<select name="perPage" onchange="this.form.submit()" class="per-page-select">
<option value="10" {{ request('perPage') == 10 ? 'selected' : '' }}>10</option>
<option value="25" {{ request('perPage') == 25 ? 'selected' : '' }}>25</option>
<option value="50" {{ request('perPage') == 50 ? 'selected' : '' }}>50</option>
<option value="10" {{ request('perPage') == 10 ? 'selected' : '' }}>10</option>
<option value="25" {{ request('perPage') == 25 ? 'selected' : '' }}>25</option>
<option value="50" {{ request('perPage') == 50 ? 'selected' : '' }}>50</option>
<option value="100" {{ request('perPage') == 100 ? 'selected' : '' }}>100</option>
</select>
<span>data</span>
<input type="hidden" name="search" value="{{ request('search') }}">
</form>
@ -162,28 +195,52 @@
<th>No</th>
<th>Nama Lengkap</th>
<th>NIP</th>
<th>Mapel</th>
<th>Kelas</th>
<th>Password</th>
<th>Aksi</th>
</tr>
</thead>
<tbody>
@forelse($gurus as $index => $guru)
<tr>
<td>{{ $gurus->firstItem() + $index }}</td>
<td>{{ $guru->nama }}</td>
<td>{{ $guru->nip }}</td>
<td>
@forelse($guru->mengajars as $m)
<div>{{ optional($m->mapel)->nama_mapel ?? '-' }}</div>
@empty
<span class="text-muted">-</span>
@endforelse
</td>
<td>
@forelse($guru->mengajars as $m)
<div>{{ optional($m->kelas)->tingkat }} {{ optional($m->kelas)->nama_kelas }}</div>
@empty
<span class="text-muted">-</span>
@endforelse
</td>
<td>********</td>
<td>
<button onclick="openEditModal('{{ $guru->nip }}', '{{ $guru->nama }}')"
style="border:none;background:none">
{{-- TOMBOL EDIT --}}
<button onclick="openEditModal(
'{{ $guru->id_guru }}',
'{{ $guru->nip }}',
'{{ addslashes($guru->nama) }}',
{{ $guru->mengajars->map(fn($m) => ['id_mapel' => $m->id_mapel, 'id_kelas' => $m->id_kelas])->toJson() }}
)" style="border:none;background:none">
<img src="{{ asset('images/icon/main/edit.png') }}" class="action-icon">
</button>
<form action="{{ route('admin.guru.destroy', $guru->id_guru) }}"
{{-- TOMBOL HAPUS --}}
<form action="{{ route('admin.guru.destroy', $guru->id_guru) }}"
method="POST" class="d-inline"
onsubmit="return confirm('Yakin ingin menghapus data?')">
onsubmit="return confirm('Yakin hapus guru ini?')">
@csrf
@method('DELETE')
<button type="submit" style="border:none;background:none">
@ -192,10 +249,9 @@
</form>
</td>
</tr>
@empty
<tr>
<td colspan="5">Belum ada data guru</td>
<td colspan="7" class="text-muted">Belum ada data guru.</td>
</tr>
@endforelse
</tbody>
@ -204,10 +260,12 @@
<div class="d-flex justify-content-end">
{{ $gurus->links() }}
</div>
</div>
{{-- MODAL TAMBAH DATA --}}
{{-- ============================================================ --}}
{{-- MODAL TAMBAH DATA --}}
{{-- ============================================================ --}}
<div class="modal fade" id="modalTambah" tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
@ -219,7 +277,6 @@
<form action="{{ route('admin.guru.store') }}" method="POST">
@csrf
<div class="modal-body">
<div class="mb-3">
@ -235,23 +292,55 @@
<div class="mb-3">
<label>Password <span class="text-danger">*</span></label>
<input type="password" name="password" class="form-control" placeholder="Minimal 6 karakter" required>
<small class="text-muted">Password akan di-hash otomatis</small>
<small class="text-muted">Password akan di-hash otomatis.</small>
</div>
{{-- PASANGAN MAPEL + KELAS --}}
<div class="mb-1">
<label>Mata Pelajaran & Kelas yang Diajar <span class="text-danger">*</span></label>
<small class="text-muted d-block mb-2">Tambahkan baris untuk setiap kombinasi mapel & kelas.</small>
</div>
<div id="tambahRows">
{{-- Baris pertama default --}}
<div class="mengajar-row">
<span class="row-label">Mapel</span>
<select name="id_mapel[]" required>
<option value="">-- Pilih Mapel --</option>
@foreach($mapels as $mapel)
<option value="{{ $mapel->id_mapel }}">{{ $mapel->nama_mapel }}</option>
@endforeach
</select>
<span class="row-label">Kelas</span>
<select name="id_kelas[]" required>
<option value="">-- Pilih Kelas --</option>
@foreach($kelas as $k)
<option value="{{ $k->id_kelas }}">{{ $k->tingkat }} {{ $k->nama_kelas }}</option>
@endforeach
</select>
<button type="button" class="btn-hapus-row" onclick="hapusRow(this)" title="Hapus baris"></button>
</div>
</div>
<button type="button" class="btn-tambah-row" onclick="tambahRow('tambahRows')">
+ Tambah Mapel/Kelas
</button>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Batal</button>
<button type="submit" class="btn btn-success">Simpan Data</button>
</div>
</form>
</div>
</div>
</div>
{{-- MODAL EDIT DATA --}}
{{-- ============================================================ --}}
{{-- MODAL EDIT DATA --}}
{{-- ============================================================ --}}
<div class="modal fade" id="modalEdit" tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
@ -268,8 +357,8 @@
<div class="modal-body">
<div class="mb-3">
<label>NIP</label>
<input type="text" id="editNip" class="form-control" disabled>
<label>NIP <span class="text-danger">*</span></label>
<input type="text" name="nip" id="editNip" class="form-control" required>
</div>
<div class="mb-3">
@ -278,32 +367,173 @@
</div>
<div class="mb-3">
<label>Password Baru (Opsional)</label>
<input type="password" name="password" class="form-control" placeholder="Isi jika ingin mengganti password">
<label>Password Baru <small class="text-muted fw-normal">(Kosongkan jika tidak ingin mengubah)</small></label>
<input type="password" name="password" class="form-control" placeholder="Minimal 6 karakter">
</div>
{{-- PASANGAN MAPEL + KELAS --}}
<div class="mb-1">
<label>Mata Pelajaran & Kelas yang Diajar <span class="text-danger">*</span></label>
<small class="text-muted d-block mb-2">Tambahkan baris untuk setiap kombinasi mapel & kelas.</small>
</div>
<div id="editRows">
{{-- Diisi oleh JavaScript --}}
</div>
<button type="button" class="btn-tambah-row" onclick="tambahRow('editRows')">
+ Tambah Mapel/Kelas
</button>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Batal</button>
<button type="submit" class="btn btn-save-pastel">Update Data</button>
<button type="submit" class="btn btn-warning">Update Data</button>
</div>
</form>
</div>
</div>
</div>
<script>
function openEditModal(nip, nama) {
document.getElementById('editNip').value = nip;
document.getElementById('editNama').value = nama;
document.getElementById('formEdit').action = "{{ url('admin/guru') }}/" + nip;
{{-- ============================================================ --}}
{{-- TEMPLATE ROW kelas auto-fill via AJAX saat mapel dipilih --}}
{{-- ============================================================ --}}
<template id="rowTemplate">
<div class="mengajar-row">
<span class="row-label">Mapel</span>
<select name="id_mapel[]" class="select-mapel" onchange="onMapelChange(this)" required>
<option value="">-- Pilih Mapel --</option>
@foreach($mapels as $mapel)
<option value="{{ $mapel->id_mapel }}">{{ $mapel->nama_mapel }}</option>
@endforeach
</select>
<span class="row-label">Kelas</span>
<select name="id_kelas[]" class="select-kelas" required disabled>
<option value="">-- Pilih mapel dulu --</option>
</select>
<button type="button" class="btn-hapus-row" onclick="hapusRow(this)" title="Hapus baris"></button>
</div>
</template>
<script>
const KELAS_BY_MAPEL_URL = "{{ route('admin.guru.kelasByMapel') }}";
// ===== SAAT MAPEL DIPILIH → fetch kelas otomatis =====
async function onMapelChange(selectMapel) {
const idMapel = selectMapel.value;
const row = selectMapel.closest('.mengajar-row');
const selectKelas = row.querySelector('.select-kelas');
// Reset kelas
selectKelas.innerHTML = '<option value="">-- Memuat kelas... --</option>';
selectKelas.disabled = true;
if (!idMapel) {
selectKelas.innerHTML = '<option value="">-- Pilih mapel dulu --</option>';
return;
}
try {
const res = await fetch(`${KELAS_BY_MAPEL_URL}?id_mapel=${idMapel}`);
const data = await res.json();
if (data.length === 0) {
selectKelas.innerHTML = '<option value="">Tidak ada kelas untuk mapel ini</option>';
return;
}
selectKelas.innerHTML = '<option value="">-- Pilih Kelas --</option>';
data.forEach(k => {
const opt = document.createElement('option');
opt.value = k.id_kelas;
opt.textContent = k.tingkat + ' ' + k.nama_kelas;
selectKelas.appendChild(opt);
});
// Jika hanya 1 kelas, langsung pilih otomatis
if (data.length === 1) {
selectKelas.value = data[0].id_kelas;
}
selectKelas.disabled = false;
} catch (e) {
selectKelas.innerHTML = '<option value="">Gagal memuat kelas</option>';
}
}
// ===== TAMBAH BARIS BARU =====
async function tambahRow(containerId, selectedMapel = null, selectedKelas = null) {
const template = document.getElementById('rowTemplate');
const clone = template.content.cloneNode(true);
const container = document.getElementById(containerId);
container.appendChild(clone);
const rows = container.querySelectorAll('.mengajar-row');
const last = rows[rows.length - 1];
const selectMapel = last.querySelector('.select-mapel');
const selectKelas = last.querySelector('.select-kelas');
if (selectedMapel) {
selectMapel.value = selectedMapel;
// Fetch kelas untuk mapel yang sudah dipilih
try {
const res = await fetch(`${KELAS_BY_MAPEL_URL}?id_mapel=${selectedMapel}`);
const data = await res.json();
selectKelas.innerHTML = '<option value="">-- Pilih Kelas --</option>';
data.forEach(k => {
const opt = document.createElement('option');
opt.value = k.id_kelas;
opt.textContent = k.tingkat + ' ' + k.nama_kelas;
selectKelas.appendChild(opt);
});
if (selectedKelas) selectKelas.value = selectedKelas;
selectKelas.disabled = false;
} catch (e) {
selectKelas.innerHTML = '<option value="">Gagal memuat kelas</option>';
}
}
}
// ===== HAPUS BARIS =====
function hapusRow(btn) {
const container = btn.closest('[id$="Rows"]');
const rows = container.querySelectorAll('.mengajar-row');
if (rows.length <= 1) {
alert('Minimal harus ada 1 mata pelajaran.');
return;
}
btn.closest('.mengajar-row').remove();
}
// ===== BUKA MODAL EDIT =====
async function openEditModal(idGuru, nip, nama, mengajars) {
document.getElementById('formEdit').action = "{{ url('admin/guru') }}/" + idGuru;
document.getElementById('editNip').value = nip;
document.getElementById('editNama').value = nama;
const editRows = document.getElementById('editRows');
editRows.innerHTML = '';
if (mengajars && mengajars.length > 0) {
for (const m of mengajars) {
await tambahRow('editRows', m.id_mapel, m.id_kelas);
}
} else {
tambahRow('editRows');
}
new bootstrap.Modal(document.getElementById('modalEdit')).show();
}
</script>
@endsection
@endsection

View File

@ -5,7 +5,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@yield('title', 'Panel Guru')</title>
<!-- Fonts & Bootstrap -->
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
@ -16,12 +15,8 @@
margin: 0;
}
.wrapper {
display: flex;
min-height: 100vh;
}
.wrapper { display: flex; min-height: 100vh; }
/* SIDEBAR */
.sidebar {
width: 260px;
background: #ffffff;
@ -31,14 +26,8 @@
flex-direction: column;
}
.sidebar-logo {
text-align: center;
margin-bottom: 40px;
}
.sidebar-logo img {
width: 90px;
}
.sidebar-logo { text-align: center; margin-bottom: 40px; }
.sidebar-logo img { width: 90px; }
.sidebar-link {
display: flex;
@ -53,35 +42,14 @@
transition: all 0.2s ease;
}
.sidebar-link:hover {
background: #e6f0ff;
color: #1d4ed8;
}
.sidebar-link:hover { background: #e6f0ff; color: #1d4ed8; }
.sidebar-link.active { background: #e6f0ff; color: #1d4ed8; }
.sidebar-link.active {
background: #e6f0ff;
color: #1d4ed8;
}
.sidebar-icon { width: 20px; height: 20px; flex-shrink: 0; }
.sidebar-logout { margin-top: auto; }
.sidebar-icon {
width: 20px;
height: 20px;
flex-shrink: 0;
}
.main { flex: 1; background: #f5f9ff; display: flex; flex-direction: column; }
.sidebar-logout {
margin-top: auto;
}
/* MAIN */
.main {
flex: 1;
background: #f5f9ff;
display: flex;
flex-direction: column;
}
/* TOPBAR */
.topbar {
background: #2b8ef3;
margin: 20px;
@ -101,37 +69,21 @@
font-size: 16px;
}
.topbar-waving {
width: 26px;
height: 26px;
}
.topbar-waving { width: 26px; height: 26px; }
.topbar-right {
display: flex;
align-items: center;
gap: 16px;
}
.topbar-right { display: flex; align-items: center; gap: 16px; }
.topbar-icon { width: 24px; height: 24px; cursor: pointer; }
.topbar-icon {
width: 24px;
height: 24px;
cursor: pointer;
}
/* CONTENT */
.content {
padding: 20px 30px;
flex: 1;
}
.content { padding: 20px 30px; flex: 1; }
</style>
@stack('styles')
</head>
<body>
<div class="wrapper">
<!-- SIDEBAR -->
<aside class="sidebar">
<div class="sidebar-logo">
<img src="{{ asset('images/logo/logosmk.png') }}" alt="Logo">
</div>
@ -174,48 +126,30 @@ class="sidebar-link {{ request()->routeIs('guru.leaderboard.*') ? 'active' : ''
<form action="{{ route('guru.logout') }}" method="POST" class="sidebar-logout">
@csrf
<button type="submit" class="btn btn-danger w-100">
Logout
</button>
<button type="submit" class="btn btn-danger w-100">Logout</button>
</form>
</aside>
<!-- MAIN -->
<div class="main">
<!-- TOPBAR -->
<header class="topbar">
<div class="topbar-left">
<img src="{{ asset('images/icon/main/hi.png') }}"
class="topbar-waving"
alt="Waving">
<img src="{{ asset('images/icon/main/hi.png') }}" class="topbar-waving" alt="Waving">
Selamat datang, {{ Auth::guard('guru')->user()->nama ?? 'Guru' }}
</div>
<div class="topbar-right">
<img src="{{ asset('images/icon/sidebar/notif.png') }}"
class="topbar-icon"
alt="Notification">
<img src="{{ asset('images/icon/sidebar/profil.png') }}"
class="topbar-icon"
alt="Profile">
<img src="{{ asset('images/icon/sidebar/notif.png') }}" class="topbar-icon" alt="Notification">
<img src="{{ asset('images/icon/sidebar/profil.png') }}" class="topbar-icon" alt="Profile">
</div>
</header>
<!-- CONTENT -->
<main class="content">
@yield('content')
</main>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
@stack('scripts')
</body>
</html>
</html>

View File

@ -2,8 +2,7 @@
@section('title', 'Daftar Mata Pelajaran')
@section('content')
@push('styles')
<style>
.page-title {
font-size: 30px;
@ -28,6 +27,9 @@
border-radius: 10px;
border: none;
font-size: 13px;
cursor: pointer;
text-decoration: none;
display: inline-block;
}
.btn-materi {
@ -35,57 +37,372 @@
color: white;
}
.btn-materi:hover { background: #1a7ae0; color: white; }
.btn-tugas {
background: #f97316;
color: white;
}
.btn-tugas:hover { background: #ea6c0a; color: white; }
.kelas-badge {
display: inline-block;
background: #e6f0ff;
color: #1d4ed8;
font-size: 12px;
font-weight: 600;
padding: 3px 10px;
border-radius: 99px;
margin: 2px;
}
/* MODAL */
.modal-header-blue {
background: #2b8ef3;
color: white;
border-bottom: none;
border-radius: 16px 16px 0 0;
}
.modal-header-orange {
background: #f97316;
color: white;
border-bottom: none;
border-radius: 16px 16px 0 0;
}
.modal-header-blue .btn-close,
.modal-header-orange .btn-close {
filter: brightness(0) invert(1);
}
.modal-content {
border-radius: 16px;
border: none;
box-shadow: 0 10px 30px rgba(0,0,0,0.15);
}
.modal-body label { font-weight: 600; font-size: 14px; }
.upload-area {
border: 2px dashed #cbd5e1;
border-radius: 12px;
padding: 24px;
text-align: center;
cursor: pointer;
position: relative;
transition: border-color 0.2s, background 0.2s;
}
.upload-area:hover { border-color: #2b8ef3; background: #f0f7ff; }
.upload-area input[type="file"] {
position: absolute;
inset: 0;
opacity: 0;
cursor: pointer;
width: 100%;
height: 100%;
}
.file-preview {
display: none;
align-items: center;
gap: 10px;
background: #f0f7ff;
border-radius: 8px;
padding: 10px 14px;
margin-top: 10px;
font-size: 13px;
font-weight: 600;
color: #1e293b;
}
.file-preview.show { display: flex; }
.alert-success-custom {
background: #dcfce7;
color: #166534;
border-radius: 10px;
padding: 12px 16px;
margin-bottom: 16px;
font-weight: 500;
font-size: 14px;
}
.alert-error-custom {
background: #fee2e2;
color: #991b1b;
border-radius: 10px;
padding: 12px 16px;
margin-bottom: 16px;
font-weight: 500;
font-size: 14px;
}
</style>
@endpush
@section('content')
<h3 class="page-title">MATA PELAJARAN YANG ANDA AJAR</h3>
<div class="custom-card">
@if(session('success'))
<div class="alert-success-custom"> {{ session('success') }}</div>
@endif
@if(session('error'))
<div class="alert-error-custom"> {{ session('error') }}</div>
@endif
<div class="custom-card">
<table class="table text-center align-middle">
<thead class="table-header">
<tr>
<th>No</th>
<th>ID Mapel</th>
<th>Nama Mata Pelajaran</th>
<th>Kelas yang Diajar</th>
<th>Aksi</th>
</tr>
</thead>
<tbody>
@forelse($mapels as $index => $mapel)
@forelse($mengajars as $idMapel => $rows)
@php
$mapel = $rows->first()->mapel;
$kelasList = $rows->map(fn($m) => $m->kelas)->filter();
// Ambil id_mengajar pertama sebagai default (bisa dipilih per kelas di modal)
$mengajarOptions = $rows->map(fn($m) => [
'id_mengajar' => $m->id_mengajar,
'kelas' => optional($m->kelas)->tingkat . ' ' . optional($m->kelas)->nama_kelas,
]);
@endphp
<tr>
<td>{{ $mapels->firstItem() + $index }}</td>
<td>{{ $mapel->id_mapel }}</td>
<td>{{ $mapel->nama_mapel }}</td>
<td>{{ $loop->iteration }}</td>
<td>{{ optional($mapel)->nama_mapel ?? '-' }}</td>
{{-- KOLOM KELAS --}}
<td>
<a href="{{ route('guru.materi.create', $mapel->id_mapel) }}"
class="action-btn btn-materi">
Upload Materi
</a>
@foreach($kelasList as $kelas)
<span class="kelas-badge">
{{ $kelas->tingkat }} {{ $kelas->nama_kelas }}
</span>
@endforeach
</td>
<a href="{{ route('guru.tugas.create', $mapel->id_mapel) }}"
class="action-btn btn-tugas">
Buat Tugas
</a>
{{-- AKSI --}}
<td>
<button class="action-btn btn-materi"
onclick="openMateriModal(
'{{ addslashes(optional($mapel)->nama_mapel) }}',
{{ $mengajarOptions->toJson() }}
)">
📄 Upload Materi
</button>
<button class="action-btn btn-tugas mt-1"
onclick="openTugasModal(
'{{ addslashes(optional($mapel)->nama_mapel) }}',
{{ $mengajarOptions->toJson() }}
)">
📋 Buat Tugas
</button>
</td>
</tr>
@empty
<tr>
<td colspan="4">Anda belum mengajar mata pelajaran apapun.</td>
<td colspan="4" class="text-muted py-4">
Anda belum mengajar mata pelajaran apapun.
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<div class="d-flex justify-content-end">
{{ $mapels->links() }}
{{-- ============================================================ --}}
{{-- MODAL UPLOAD MATERI --}}
{{-- ============================================================ --}}
<div class="modal fade" id="modalMateri" tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-header modal-header-blue">
<h5 class="modal-title">📄 Upload Materi <span id="materiMapelLabel"></span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form action="{{ route('guru.materi.store') }}" method="POST" enctype="multipart/form-data">
@csrf
<div class="modal-body">
{{-- Pilih Kelas (jika guru ajar mapel ini di lebih dari 1 kelas) --}}
<div class="mb-3">
<label>Kelas Tujuan <span class="text-danger">*</span></label>
<select name="id_mengajar" id="materiMengajar" class="form-control" required>
<option value="">-- Pilih Kelas --</option>
</select>
<small class="text-muted">Materi akan dikirim ke kelas yang dipilih.</small>
</div>
<div class="mb-3">
<label>Judul Materi <span class="text-danger">*</span></label>
<input type="text" name="judul_materi" class="form-control"
placeholder="Contoh: Pertemuan 1 - Pengantar Algoritma" required>
</div>
<div class="mb-3">
<label>Deskripsi</label>
<textarea name="deskripsi" class="form-control" rows="3"
placeholder="Deskripsi singkat materi (opsional)"></textarea>
</div>
<div class="mb-3">
<label>File Materi <small class="text-muted fw-normal">(PDF, DOC, PPT, JPG maks 10MB)</small></label>
<div class="upload-area" id="materiUploadArea">
<input type="file" name="lampiran_materi" id="materiFile"
accept=".pdf,.doc,.docx,.ppt,.pptx,.jpg,.jpeg,.png"
onchange="previewFile(this, 'materiPreview', 'materiFileName')">
<div style="font-size:32px">☁️</div>
<p style="margin:6px 0 0;font-size:14px;color:#64748b">
<strong>Klik</strong> atau drag file ke sini
</p>
</div>
<div class="file-preview" id="materiPreview">
📎 <span id="materiFileName">-</span>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Batal</button>
<button type="submit" class="btn btn-primary">Upload Materi</button>
</div>
</form>
</div>
</div>
</div>
{{-- ============================================================ --}}
{{-- MODAL BUAT TUGAS --}}
{{-- ============================================================ --}}
<div class="modal fade" id="modalTugas" tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-header modal-header-orange">
<h5 class="modal-title">📋 Buat Tugas <span id="tugasMapelLabel"></span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form action="{{ route('guru.tugas.store') }}" method="POST">
@csrf
<div class="modal-body">
{{-- Pilih Kelas --}}
<div class="mb-3">
<label>Kelas Tujuan <span class="text-danger">*</span></label>
<select name="id_mengajar" id="tugasMengajar" class="form-control" required>
<option value="">-- Pilih Kelas --</option>
</select>
<small class="text-muted">Tugas akan dikirim ke kelas yang dipilih.</small>
</div>
<div class="mb-3">
<label>Judul Tugas <span class="text-danger">*</span></label>
<input type="text" name="judul_tugas" class="form-control"
placeholder="Contoh: Latihan Soal Bab 3" required>
</div>
<div class="mb-3">
<label>Keterangan / Instruksi</label>
<textarea name="keterangan" class="form-control" rows="4"
placeholder="Instruksi pengerjaan tugas (opsional)"></textarea>
</div>
<div class="mb-3">
<label>Deadline <span class="text-danger">*</span></label>
<input type="datetime-local" name="deadline" class="form-control" required
min="{{ now()->format('Y-m-d\TH:i') }}">
<small class="text-muted">Pastikan deadline lebih dari waktu sekarang.</small>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Batal</button>
<button type="submit" class="btn" style="background:#f97316;color:white">Buat Tugas</button>
</div>
</form>
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
// ===== BUKA MODAL MATERI =====
function openMateriModal(namaMapel, mengajars) {
document.getElementById('materiMapelLabel').textContent = namaMapel;
const select = document.getElementById('materiMengajar');
select.innerHTML = '<option value="">-- Pilih Kelas --</option>';
mengajars.forEach(m => {
const opt = document.createElement('option');
opt.value = m.id_mengajar;
opt.textContent = m.kelas;
select.appendChild(opt);
});
// Jika hanya 1 kelas, langsung pilih otomatis
if (mengajars.length === 1) select.value = mengajars[0].id_mengajar;
// Reset form
document.querySelector('#modalMateri input[name="judul_materi"]').value = '';
document.querySelector('#modalMateri textarea[name="deskripsi"]').value = '';
document.getElementById('materiPreview').classList.remove('show');
new bootstrap.Modal(document.getElementById('modalMateri')).show();
}
// ===== BUKA MODAL TUGAS =====
function openTugasModal(namaMapel, mengajars) {
document.getElementById('tugasMapelLabel').textContent = namaMapel;
const select = document.getElementById('tugasMengajar');
select.innerHTML = '<option value="">-- Pilih Kelas --</option>';
mengajars.forEach(m => {
const opt = document.createElement('option');
opt.value = m.id_mengajar;
opt.textContent = m.kelas;
select.appendChild(opt);
});
if (mengajars.length === 1) select.value = mengajars[0].id_mengajar;
// Reset form
document.querySelector('#modalTugas input[name="judul_tugas"]').value = '';
document.querySelector('#modalTugas textarea[name="keterangan"]').value = '';
document.querySelector('#modalTugas input[name="deadline"]').value = '';
new bootstrap.Modal(document.getElementById('modalTugas')).show();
}
// ===== PREVIEW FILE =====
function previewFile(input, previewId, nameId) {
const preview = document.getElementById(previewId);
const nameEl = document.getElementById(nameId);
if (input.files && input.files[0]) {
nameEl.textContent = input.files[0].name +
' (' + (input.files[0].size / 1024 / 1024).toFixed(2) + ' MB)';
preview.classList.add('show');
}
}
</script>
@endpush

View File

@ -89,7 +89,6 @@
.sidebar-logout {
margin-top: auto;
white-space: nowrap;
}
.sidebar-logout button {
@ -100,7 +99,6 @@
font-weight: 600;
padding: 10px;
text-align: left;
cursor: pointer;
}
/* ===== TOGGLE ARROW BUTTON ===== */
@ -241,18 +239,13 @@ class="sidebar-link {{ request()->routeIs('siswa.leaderboard*') ? 'active' : ''
<img src="{{ asset('images/icon/sidebar/lb.png') }}" class="sidebar-icon" alt="">
<span>Leaderboard</span>
</a>
<a href="#"
class="sidebar-link {{ request()->routeIs('siswa.profil*') ? 'active' : '' }}">
<img src="{{ asset('images/icon/sidebar/profil.png') }}" class="sidebar-icon" alt="">
<span>Profil</span>
</a>
</nav>
<form action="{{ route('siswa.logout') }}" method="POST" class="sidebar-logout">
@csrf
<button type="submit">Logout</button>
<button type="submit" class="btn btn-danger w-100">
Logout
</button>
</form>
</aside>

View File

@ -87,10 +87,11 @@
Route::get('/profil', function () {
return view('admin.profil');
})->name('profil');
})->name('profil');
// CRUD AREA
Route::get('/guru/kelas-by-mapel', [AdminGuruController::class, 'getKelasByMapel'])
->name('guru.kelasByMapel');
Route::resource('guru', AdminGuruController::class);
Route::resource('siswa', AdminSiswaController::class);
Route::resource('kelas', AdminKelasController::class);
@ -98,6 +99,7 @@
Route::resource('leaderboard', AdminLeaderboardController::class)
->only(['index']);
Route::resource('challenge', AdminChallengeController::class);
// LOGOUT ADMIN
Route::post('/logout', [LoginController::class, 'logout'])
@ -123,6 +125,8 @@
Route::get('/mapel', [GuruMapelController::class, 'index'])
->name('mapel.index');
Route::post('/materi/store', [GuruMapelController::class, 'storeMateri'])->name('materi.store');
Route::post('/tugas/store', [GuruMapelController::class, 'storeTugas'])->name('tugas.store');
Route::get('/leaderboard', [GuruLeaderboardController::class, 'index'])
->name('leaderboard.index');