materi tugas siswa

This commit is contained in:
RetasyaSalsabila 2026-03-05 11:53:15 +07:00
parent bc5a63ade9
commit c6e706c4e1
11 changed files with 1365 additions and 44 deletions

View File

@ -6,18 +6,15 @@
use Illuminate\Support\Facades\Auth;
use Carbon\Carbon;
// Sesuaikan nama model-model ini dengan yang kamu punya
use App\Models\Tugas;
use App\Models\TugasSiswa; // tabel pengumpulan tugas
use App\Models\PengumpulanTugas;
use App\Models\Challenge;
use App\Models\ChallengeSiswa; // tabel progress challenge siswa
use App\Models\PesertaChallenge;
use App\Models\Leaderboard;
use App\Models\Siswa;
class DashboardController extends Controller
{
/**
* Pastikan hanya siswa yang sudah login yang bisa akses.
*/
public function __construct()
{
$this->middleware('auth:siswa');
@ -29,72 +26,83 @@ public function index()
$siswa = Auth::guard('siswa')->user();
// =============================================
// 1. TUGAS — ambil tugas yang belum dikumpulkan
// dan deadline-nya belum lewat, urutkan by deadline
// 1. TUGAS — yang belum dikumpulkan siswa ini
// dan deadline belum lewat
// =============================================
$tugasRaw = Tugas::with('mataPelajaran') // eager load relasi mapel
->whereDoesntHave('pengumpulan', function ($q) use ($siswa) {
// tugas yang BELUM dikumpulkan oleh siswa ini
$q->where('siswa_id', $siswa->id);
})
// id_tugas yang sudah dikumpulkan siswa ini
$sudahDikumpulkan = PengumpulanTugas::where('id_siswa', $siswa->id_siswa)
->pluck('id_tugas');
// Tugas untuk kelas siswa yang belum dikumpulkan
$tugasRaw = Tugas::with(['mengajar.mapel'])
->whereNotIn('id_tugas', $sudahDikumpulkan)
->where('deadline', '>=', Carbon::now())
->whereHas('mengajar', function ($q) use ($siswa) {
$q->where('id_kelas', $siswa->id_kelas);
})
->orderBy('deadline', 'asc')
->take(5) // tampilkan maks 5 tugas di dashboard
->take(5)
->get();
// Kelompokkan tugas berdasarkan tanggal deadline
// Kelompokkan per tanggal deadline
$tugasList = [];
foreach ($tugasRaw as $tugas) {
$tgl = Carbon::parse($tugas->deadline)
->locale('id')
->isoFormat('dddd, D MMMM YYYY'); // contoh: "Sabtu, 10 Mei 2025"
->isoFormat('dddd, D MMMM YYYY');
$namaMapel = optional(optional($tugas->mengajar)->mapel)->nama_mapel ?? '-';
$tugasList[$tgl][] = [
'jam' => Carbon::parse($tugas->deadline)->format('H.i'),
'nama' => $tugas->judul,
// sesuaikan nama kolom dengan model kamu
'mapel' => 'Belum · ' . ($tugas->mataPelajaran->nama ?? '-'),
'nama' => $tugas->judul_tugas,
'mapel' => 'Belum · ' . $namaMapel,
];
}
// =============================================
// 2. CHALLENGE MINGGUAN
// =============================================
$startMingguIni = Carbon::now()->startOfWeek();
$endMingguIni = Carbon::now()->endOfWeek();
$startMinggu = Carbon::now()->startOfWeek();
$endMinggu = Carbon::now()->endOfWeek();
// Total challenge aktif minggu ini
$challengeTotal = Challenge::whereBetween('created_at', [$startMingguIni, $endMingguIni])
->where('is_active', true)
// Challenge untuk kelas siswa yang masih aktif minggu ini
$challengeTotal = Challenge::whereHas('kelas', function ($q) use ($siswa) {
$q->where('challenge_kelas.id_kelas', $siswa->id_kelas);
})
->where('tenggat_waktu', '>=', Carbon::now())
->whereBetween('created_at', [$startMinggu, $endMinggu])
->count();
// Challenge yang sudah diselesaikan siswa minggu ini
$challengeDone = ChallengeSiswa::where('siswa_id', $siswa->id)
// Challenge yang sudah selesai dikerjakan siswa minggu ini
$challengeDone = PesertaChallenge::where('id_siswa', $siswa->id_siswa)
->where('status', 'selesai')
->whereBetween('created_at', [$startMingguIni, $endMingguIni])
->whereBetween('waktu_submit', [$startMinggu, $endMinggu])
->count();
// =============================================
// 3. TUGAS SELESAI MINGGU INI (untuk speech bubble mascot)
// 3. TUGAS SELESAI MINGGU INI (mascot speech bubble)
// =============================================
$tugasSelesai = TugasSiswa::where('siswa_id', $siswa->id)
->whereBetween('created_at', [$startMingguIni, $endMingguIni])
$tugasSelesai = PengumpulanTugas::where('id_siswa', $siswa->id_siswa)
->whereIn('status', ['dikumpulkan', 'terlambat'])
->whereBetween('tanggal_submit', [$startMinggu, $endMinggu])
->count();
// =============================================
// 4. LEADERBOARD — top 3 siswa berdasarkan total EXP
// Asumsi: kolom 'exp' ada di tabel siswa
// Sesuaikan jika EXP disimpan di tabel lain
// 4. LEADERBOARD — top 3 kelas siswa yang login
// =============================================
$leaderboardRaw = Siswa::orderBy('exp', 'desc')
$leaderboardRaw = Leaderboard::with('siswa')
->where('id_kelas', $siswa->id_kelas)
->orderBy('total_exp', 'desc')
->take(3)
->get();
$leaderboard = $leaderboardRaw->map(function ($item, $index) {
return [
'rank' => $index + 1,
'nama' => $item->nama,
'exp' => $item->exp,
'nama' => optional($item->siswa)->nama ?? '-',
'exp' => $item->total_exp,
];
})->toArray();

View File

@ -0,0 +1,59 @@
<?php
namespace App\Http\Controllers\Siswa;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Auth;
use App\Models\Mengajar;
use App\Models\Materi;
class MateriController extends Controller
{
public function __construct()
{
$this->middleware('auth:siswa');
}
/**
* Halaman daftar mata pelajaran siswa
*/
public function index()
{
$siswa = Auth::guard('siswa')->user();
// Ambil semua mapel yang diajarkan di kelas siswa ini
$mapelList = Mengajar::with(['mapel', 'guru'])
->where('id_kelas', $siswa->id_kelas)
->get()
->map(function ($mengajar) {
return [
'id_mengajar' => $mengajar->id_mengajar,
'nama_mapel' => optional($mengajar->mapel)->nama_mapel ?? '-',
'nama_guru' => optional($mengajar->guru)->nama ?? '-',
'jumlah_materi' => Materi::where('id_mengajar', $mengajar->id_mengajar)->count(),
];
});
return view('siswa.materi.index', compact('mapelList'));
}
/**
* Halaman daftar materi per mata pelajaran
*/
public function show($id_mengajar)
{
$siswa = Auth::guard('siswa')->user();
// Pastikan mengajar ini memang untuk kelas siswa
$mengajar = Mengajar::with(['mapel', 'guru'])
->where('id_mengajar', $id_mengajar)
->where('id_kelas', $siswa->id_kelas)
->firstOrFail();
$materiList = Materi::where('id_mengajar', $id_mengajar)
->orderBy('tanggal_upload', 'desc')
->get();
return view('siswa.materi.show', compact('mengajar', 'materiList'));
}
}

View File

@ -0,0 +1,152 @@
<?php
namespace App\Http\Controllers\Siswa;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Carbon\Carbon;
use App\Models\Tugas;
use App\Models\PengumpulanTugas;
use App\Models\Mengajar;
class TugasController extends Controller
{
public function __construct()
{
$this->middleware('auth:siswa');
}
/**
* Daftar semua tugas untuk kelas siswa
*/
public function index()
{
$siswa = Auth::guard('siswa')->user();
// Ambil semua tugas untuk kelas siswa
$semuaTugas = Tugas::with(['mengajar.mapel', 'mengajar.guru'])
->whereHas('mengajar', function ($q) use ($siswa) {
$q->where('id_kelas', $siswa->id_kelas);
})
->orderBy('deadline', 'asc')
->get();
// Tandai status tiap tugas untuk siswa ini
$tugasList = $semuaTugas->map(function ($tugas) use ($siswa) {
$pengumpulan = PengumpulanTugas::where('id_tugas', $tugas->id_tugas)
->where('id_siswa', $siswa->id_siswa)
->first();
$now = Carbon::now();
$deadline = Carbon::parse($tugas->deadline);
if ($pengumpulan) {
$status = $pengumpulan->status; // 'dikumpulkan' atau 'terlambat'
} else {
$status = $now->greaterThan($deadline) ? 'terlambat' : 'belum';
}
return [
'id_tugas' => $tugas->id_tugas,
'judul' => $tugas->judul_tugas,
'keterangan' => $tugas->keterangan,
'deadline' => $deadline,
'nama_mapel' => optional(optional($tugas->mengajar)->mapel)->nama_mapel ?? '-',
'nama_guru' => optional(optional($tugas->mengajar)->guru)->nama ?? '-',
'status' => $status,
'sudah_kumpul'=> !is_null($pengumpulan),
'lampiran' => $pengumpulan?->lampiran_tugas,
'exp' => $pengumpulan?->exp ?? 0,
];
});
// Kelompokkan: belum & pending vs sudah dikumpulkan
$tugasBelum = $tugasList->filter(fn($t) => !$t['sudah_kumpul'] && $t['status'] !== 'terlambat');
$tugasTerlambat = $tugasList->filter(fn($t) => !$t['sudah_kumpul'] && $t['status'] === 'terlambat');
$tugasSelesai = $tugasList->filter(fn($t) => $t['sudah_kumpul']);
return view('siswa.tugas.index', compact('tugasBelum', 'tugasTerlambat', 'tugasSelesai'));
}
/**
* Detail tugas + form submit
*/
public function show($id_tugas)
{
$siswa = Auth::guard('siswa')->user();
$tugas = Tugas::with(['mengajar.mapel', 'mengajar.guru'])
->whereHas('mengajar', function ($q) use ($siswa) {
$q->where('id_kelas', $siswa->id_kelas);
})
->where('id_tugas', $id_tugas)
->firstOrFail();
$pengumpulan = PengumpulanTugas::where('id_tugas', $id_tugas)
->where('id_siswa', $siswa->id_siswa)
->first();
$sudahKumpul = !is_null($pengumpulan);
$terlambat = Carbon::now()->greaterThan(Carbon::parse($tugas->deadline));
return view('siswa.tugas.show', compact('tugas', 'pengumpulan', 'sudahKumpul', 'terlambat'));
}
/**
* Submit / upload jawaban tugas
*/
public function submit(Request $request, $id_tugas)
{
$siswa = Auth::guard('siswa')->user();
$request->validate([
'lampiran_tugas' => 'required|file|mimes:pdf,doc,docx,jpg,jpeg,png|max:5120',
], [
'lampiran_tugas.required' => 'File jawaban wajib diupload.',
'lampiran_tugas.mimes' => 'Format file harus pdf, doc, docx, jpg, atau png.',
'lampiran_tugas.max' => 'Ukuran file maksimal 5MB.',
]);
// Cek tugas valid untuk kelas siswa
$tugas = Tugas::whereHas('mengajar', function ($q) use ($siswa) {
$q->where('id_kelas', $siswa->id_kelas);
})
->where('id_tugas', $id_tugas)
->firstOrFail();
// Cek sudah dikumpulkan belum
$sudahAda = PengumpulanTugas::where('id_tugas', $id_tugas)
->where('id_siswa', $siswa->id_siswa)
->exists();
if ($sudahAda) {
return back()->with('error', 'Kamu sudah mengumpulkan tugas ini.');
}
// Upload file
$file = $request->file('lampiran_tugas');
$filename = 'tugas_' . $siswa->id_siswa . '_' . $id_tugas . '_' . time() . '.' . $file->getClientOriginalExtension();
$path = $file->storeAs('pengumpulan_tugas', $filename, 'public');
// Tentukan status: terlambat atau dikumpulkan
$now = Carbon::now();
$deadline = Carbon::parse($tugas->deadline);
$status = $now->greaterThan($deadline) ? 'terlambat' : 'dikumpulkan';
PengumpulanTugas::create([
'id_tugas' => $id_tugas,
'id_siswa' => $siswa->id_siswa,
'lampiran_tugas' => $path,
'tanggal_submit' => $now,
'exp' => 0, // exp diberikan guru saat menilai
'status' => $status,
]);
$pesan = $status === 'terlambat'
? 'Tugas berhasil dikumpulkan (terlambat).'
: 'Tugas berhasil dikumpulkan! 🎉';
return redirect()->route('siswa.tugas.index')->with('success', $pesan);
}
}

View File

@ -9,7 +9,7 @@ class Materi extends Model
{
use HasFactory;
protected $table = 'materis';
protected $table = 'materis';
protected $primaryKey = 'id_materi';
protected $fillable = [
@ -19,4 +19,13 @@ class Materi extends Model
'deskripsi',
'tanggal_upload',
];
protected $casts = [
'tanggal_upload' => 'datetime',
];
public function mengajar()
{
return $this->belongsTo(Mengajar::class, 'id_mengajar', 'id_mengajar');
}
}

View File

@ -9,7 +9,7 @@ class Tugas extends Model
{
use HasFactory;
protected $table = 'tugas';
protected $table = 'tugas';
protected $primaryKey = 'id_tugas';
protected $fillable = [
@ -18,4 +18,24 @@ class Tugas extends Model
'keterangan',
'deadline',
];
protected $casts = [
'deadline' => 'datetime',
];
/**
* Tugas dibuat melalui relasi mengajar (guru - mapel - kelas)
*/
public function mengajar()
{
return $this->belongsTo(Mengajar::class, 'id_mengajar', 'id_mengajar');
}
/**
* Satu tugas bisa punya banyak pengumpulan dari siswa
*/
public function pengumpulanTugas()
{
return $this->hasMany(PengumpulanTugas::class, 'id_tugas', 'id_tugas');
}
}

View File

@ -1,4 +1,4 @@
@extends('layouts.siswa.app')
@extends('siswa.layouts.app')
@section('title', 'Dashboard Siswa')
@ -319,7 +319,7 @@
<div class="dash-card">
<div class="card-header">
<h2 class="card-title">Tugas</h2>
<a href="{{ route('siswa.tugas') }}" class="card-link">LIHAT SEMUA</a>
<a href="#" class="card-link">LIHAT SEMUA</a>
</div>
@forelse($tugasList as $tanggal => $items)
@ -381,7 +381,7 @@
</div>
<div class="challenge-footer">
<a href="{{ route('siswa.challenge') }}" class="card-link">LIHAT SEMUA</a>
<a href="#" class="card-link">LIHAT SEMUA</a>
</div>
</div>
@ -408,7 +408,7 @@
<div class="dash-card" style="grid-column: 1 / -1;">
<div class="card-header">
<h2 class="card-title">Leaderboard</h2>
<a href="{{ route('siswa.leaderboard') }}" class="card-link">LIHAT SEMUA</a>
<a href="#" class="card-link">LIHAT SEMUA</a>
</div>
@forelse($leaderboard as $lb)

View File

@ -218,13 +218,13 @@ class="sidebar-link {{ request()->routeIs('siswa.dashboard') ? 'active' : '' }}"
<span>Dashboard</span>
</a>
<a href="#"
<a href="{{ route('siswa.materi.index') }}"
class="sidebar-link {{ request()->routeIs('siswa.materi*') ? 'active' : '' }}">
<img src="{{ asset('images/icon/sidebar/mapel.png') }}" class="sidebar-icon" alt="">
<span>Materi</span>
</a>
<a href="#"
<a href="{{ route('siswa.tugas.index') }}"
class="sidebar-link {{ request()->routeIs('siswa.tugas*') ? 'active' : '' }}">
<img src="{{ asset('images/icon/sidebar/siswa.png') }}" class="sidebar-icon" alt="">
<span>Tugas</span>

View File

@ -0,0 +1,116 @@
@extends('layouts.siswa.app')
@section('title', 'Materi')
@push('styles')
<style>
.page-title {
font-size: 22px;
font-weight: 700;
color: #1e293b;
margin-bottom: 24px;
}
.mapel-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 20px;
}
.mapel-card {
background: #fff;
border-radius: 20px;
padding: 24px;
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
text-decoration: none;
color: inherit;
display: flex;
flex-direction: column;
gap: 12px;
transition: transform 0.2s ease, box-shadow 0.2s ease;
border-top: 4px solid #2b8ef3;
}
.mapel-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(43,142,243,0.15);
color: inherit;
}
.mapel-icon {
width: 48px;
height: 48px;
background: #e6f0ff;
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
.mapel-nama {
font-size: 16px;
font-weight: 700;
color: #1e293b;
margin: 0;
}
.mapel-guru {
font-size: 13px;
color: #64748b;
margin: 0;
}
.mapel-badge {
display: inline-flex;
align-items: center;
gap: 6px;
background: #e6f0ff;
color: #2b8ef3;
font-size: 12px;
font-weight: 600;
padding: 4px 10px;
border-radius: 99px;
width: fit-content;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #94a3b8;
}
.empty-state .empty-icon {
font-size: 48px;
margin-bottom: 12px;
}
</style>
@endpush
@section('content')
<h1 class="page-title">📚 Mata Pelajaran</h1>
@if($mapelList->isEmpty())
<div class="empty-state">
<div class="empty-icon">📭</div>
<p>Belum ada mata pelajaran untuk kelasmu.</p>
</div>
@else
<div class="mapel-grid">
@foreach($mapelList as $mapel)
<a href="{{ route('siswa.materi.show', $mapel['id_mengajar']) }}" class="mapel-card">
<div class="mapel-icon">📖</div>
<div>
<p class="mapel-nama">{{ $mapel['nama_mapel'] }}</p>
<p class="mapel-guru">{{ $mapel['nama_guru'] }}</p>
</div>
<div class="mapel-badge">
📄 {{ $mapel['jumlah_materi'] }} Materi
</div>
</a>
@endforeach
</div>
@endif
@endsection

View File

@ -0,0 +1,222 @@
@extends('layouts.siswa.app')
@section('title', 'Materi - ' . optional(optional($mengajar)->mapel)->nama_mapel)
@push('styles')
<style>
.back-link {
display: inline-flex;
align-items: center;
gap: 8px;
color: #2b8ef3;
font-weight: 600;
font-size: 14px;
text-decoration: none;
margin-bottom: 20px;
}
.back-link:hover { text-decoration: underline; }
.mapel-header {
background: linear-gradient(135deg, #2b8ef3, #60a5fa);
border-radius: 20px;
padding: 24px 28px;
color: white;
margin-bottom: 24px;
display: flex;
align-items: center;
gap: 16px;
}
.mapel-header-icon {
font-size: 36px;
background: rgba(255,255,255,0.2);
width: 60px;
height: 60px;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.mapel-header-title {
font-size: 20px;
font-weight: 700;
margin: 0 0 4px;
}
.mapel-header-sub {
font-size: 14px;
opacity: 0.85;
margin: 0;
}
.materi-list {
display: flex;
flex-direction: column;
gap: 14px;
}
.materi-card {
background: #fff;
border-radius: 16px;
padding: 20px 24px;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
display: flex;
align-items: center;
gap: 16px;
transition: box-shadow 0.2s;
}
.materi-card:hover {
box-shadow: 0 4px 20px rgba(43,142,243,0.12);
}
.materi-file-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
flex-shrink: 0;
}
.materi-file-icon.pdf { background: #fee2e2; }
.materi-file-icon.doc { background: #dbeafe; }
.materi-file-icon.img { background: #dcfce7; }
.materi-file-icon.other { background: #f1f5f9; }
.materi-info { flex: 1; }
.materi-judul {
font-size: 15px;
font-weight: 600;
color: #1e293b;
margin: 0 0 4px;
}
.materi-meta {
font-size: 12px;
color: #94a3b8;
margin: 0;
}
.materi-desc {
font-size: 13px;
color: #64748b;
margin: 6px 0 0;
}
.btn-download {
display: inline-flex;
align-items: center;
gap: 6px;
background: #2b8ef3;
color: white;
border-radius: 10px;
padding: 8px 16px;
font-size: 13px;
font-weight: 600;
text-decoration: none;
transition: background 0.2s;
white-space: nowrap;
flex-shrink: 0;
}
.btn-download:hover {
background: #1a7ae0;
color: white;
}
.btn-no-file {
background: #f1f5f9;
color: #94a3b8;
border-radius: 10px;
padding: 8px 16px;
font-size: 13px;
font-weight: 600;
white-space: nowrap;
flex-shrink: 0;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #94a3b8;
}
.empty-state .empty-icon { font-size: 48px; margin-bottom: 12px; }
</style>
@endpush
@section('content')
<a href="{{ route('siswa.materi.index') }}" class="back-link"> Kembali ke Mata Pelajaran</a>
<div class="mapel-header">
<div class="mapel-header-icon">📖</div>
<div>
<p class="mapel-header-title">{{ optional($mengajar->mapel)->nama_mapel ?? '-' }}</p>
<p class="mapel-header-sub">Guru: {{ optional($mengajar->guru)->nama ?? '-' }} &bull; {{ $materiList->count() }} Materi</p>
</div>
</div>
@if($materiList->isEmpty())
<div class="empty-state">
<div class="empty-icon">📭</div>
<p>Belum ada materi yang diupload untuk mata pelajaran ini.</p>
</div>
@else
<div class="materi-list">
@foreach($materiList as $materi)
@php
$ext = $materi->lampiran_materi
? strtolower(pathinfo($materi->lampiran_materi, PATHINFO_EXTENSION))
: null;
$iconClass = match(true) {
in_array($ext, ['pdf']) => 'pdf',
in_array($ext, ['doc','docx']) => 'doc',
in_array($ext, ['jpg','jpeg','png']) => 'img',
default => 'other',
};
$iconEmoji = match($iconClass) {
'pdf' => '📄',
'doc' => '📝',
'img' => '🖼️',
default => '📎',
};
@endphp
<div class="materi-card">
<div class="materi-file-icon {{ $iconClass }}">{{ $iconEmoji }}</div>
<div class="materi-info">
<p class="materi-judul">{{ $materi->judul_materi }}</p>
<p class="materi-meta">
Diupload {{ \Carbon\Carbon::parse($materi->tanggal_upload)->locale('id')->isoFormat('D MMMM YYYY') }}
</p>
@if($materi->deskripsi)
<p class="materi-desc">{{ $materi->deskripsi }}</p>
@endif
</div>
@if($materi->lampiran_materi)
<a href="{{ asset('storage/' . $materi->lampiran_materi) }}"
target="_blank"
class="btn-download">
⬇️ Unduh
</a>
@else
<span class="btn-no-file">Tidak ada file</span>
@endif
</div>
@endforeach
</div>
@endif
@endsection

View File

@ -0,0 +1,326 @@
@extends('layouts.siswa.app')
@section('title', 'Tugas')
@push('styles')
<style>
.page-title {
font-size: 22px;
font-weight: 700;
color: #1e293b;
margin-bottom: 24px;
}
/* TABS */
.tab-nav {
display: flex;
gap: 8px;
margin-bottom: 24px;
border-bottom: 2px solid #e2e8f0;
padding-bottom: 0;
}
.tab-btn {
background: none;
border: none;
padding: 10px 20px;
font-size: 14px;
font-weight: 600;
color: #94a3b8;
cursor: pointer;
border-bottom: 3px solid transparent;
margin-bottom: -2px;
transition: all 0.2s;
border-radius: 8px 8px 0 0;
display: flex;
align-items: center;
gap: 8px;
}
.tab-btn.active {
color: #2b8ef3;
border-bottom-color: #2b8ef3;
}
.tab-btn:hover:not(.active) { color: #475569; }
.tab-count {
background: #e6f0ff;
color: #2b8ef3;
font-size: 11px;
font-weight: 700;
padding: 2px 8px;
border-radius: 99px;
}
.tab-btn.active .tab-count {
background: #2b8ef3;
color: white;
}
/* TAB PANELS */
.tab-panel { display: none; }
.tab-panel.active { display: block; }
/* TUGAS CARD */
.tugas-list {
display: flex;
flex-direction: column;
gap: 14px;
}
.tugas-card {
background: #fff;
border-radius: 16px;
padding: 20px 24px;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
display: flex;
align-items: center;
gap: 16px;
text-decoration: none;
color: inherit;
transition: box-shadow 0.2s, transform 0.2s;
border-left: 4px solid #e2e8f0;
}
.tugas-card:hover {
box-shadow: 0 4px 20px rgba(43,142,243,0.12);
transform: translateX(4px);
color: inherit;
}
.tugas-card.status-belum { border-left-color: #2b8ef3; }
.tugas-card.status-terlambat { border-left-color: #ef4444; }
.tugas-card.status-selesai { border-left-color: #22c55e; }
.tugas-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
flex-shrink: 0;
}
.status-belum .tugas-icon { background: #e6f0ff; }
.status-terlambat .tugas-icon { background: #fee2e2; }
.status-selesai .tugas-icon { background: #dcfce7; }
.tugas-info { flex: 1; }
.tugas-judul {
font-size: 15px;
font-weight: 600;
color: #1e293b;
margin: 0 0 4px;
}
.tugas-meta {
font-size: 12px;
color: #94a3b8;
margin: 0;
}
.tugas-right {
text-align: right;
flex-shrink: 0;
}
.deadline-label {
font-size: 12px;
color: #94a3b8;
margin: 0 0 4px;
}
.deadline-val {
font-size: 13px;
font-weight: 600;
color: #475569;
margin: 0;
}
.status-badge {
display: inline-block;
font-size: 11px;
font-weight: 700;
padding: 3px 10px;
border-radius: 99px;
margin-top: 6px;
}
.badge-belum { background: #e6f0ff; color: #2b8ef3; }
.badge-terlambat { background: #fee2e2; color: #ef4444; }
.badge-selesai { background: #dcfce7; color: #16a34a; }
.exp-badge {
display: inline-flex;
align-items: center;
gap: 4px;
background: #fef9c3;
color: #b45309;
font-size: 12px;
font-weight: 700;
padding: 3px 10px;
border-radius: 99px;
margin-top: 6px;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #94a3b8;
}
.empty-icon { font-size: 48px; margin-bottom: 12px; }
/* ALERT */
.alert-success {
background: #dcfce7;
color: #166534;
border-radius: 12px;
padding: 14px 18px;
margin-bottom: 20px;
font-weight: 500;
font-size: 14px;
}
.alert-error {
background: #fee2e2;
color: #991b1b;
border-radius: 12px;
padding: 14px 18px;
margin-bottom: 20px;
font-weight: 500;
font-size: 14px;
}
</style>
@endpush
@section('content')
<h1 class="page-title">📋 Tugas</h1>
@if(session('success'))
<div class="alert-success"> {{ session('success') }}</div>
@endif
@if(session('error'))
<div class="alert-error"> {{ session('error') }}</div>
@endif
{{-- TABS --}}
<div class="tab-nav">
<button class="tab-btn active" onclick="switchTab('belum', this)">
Belum Dikerjakan
<span class="tab-count">{{ $tugasBelum->count() }}</span>
</button>
<button class="tab-btn" onclick="switchTab('terlambat', this)">
Terlambat
<span class="tab-count">{{ $tugasTerlambat->count() }}</span>
</button>
<button class="tab-btn" onclick="switchTab('selesai', this)">
Sudah Dikumpulkan
<span class="tab-count">{{ $tugasSelesai->count() }}</span>
</button>
</div>
{{-- TAB: BELUM --}}
<div id="tab-belum" class="tab-panel active">
@if($tugasBelum->isEmpty())
<div class="empty-state">
<div class="empty-icon">🎉</div>
<p>Semua tugas sudah dikerjakan!</p>
</div>
@else
<div class="tugas-list">
@foreach($tugasBelum as $tugas)
<a href="{{ route('siswa.tugas.show', $tugas['id_tugas']) }}"
class="tugas-card status-belum">
<div class="tugas-icon">📋</div>
<div class="tugas-info">
<p class="tugas-judul">{{ $tugas['judul'] }}</p>
<p class="tugas-meta">{{ $tugas['nama_mapel'] }} &bull; {{ $tugas['nama_guru'] }}</p>
</div>
<div class="tugas-right">
<p class="deadline-label">Deadline</p>
<p class="deadline-val">{{ $tugas['deadline']->format('d M Y H:i') }}</p>
<span class="status-badge badge-belum">Belum</span>
</div>
</a>
@endforeach
</div>
@endif
</div>
{{-- TAB: TERLAMBAT --}}
<div id="tab-terlambat" class="tab-panel">
@if($tugasTerlambat->isEmpty())
<div class="empty-state">
<div class="empty-icon"></div>
<p>Tidak ada tugas terlambat!</p>
</div>
@else
<div class="tugas-list">
@foreach($tugasTerlambat as $tugas)
<a href="{{ route('siswa.tugas.show', $tugas['id_tugas']) }}"
class="tugas-card status-terlambat">
<div class="tugas-icon"></div>
<div class="tugas-info">
<p class="tugas-judul">{{ $tugas['judul'] }}</p>
<p class="tugas-meta">{{ $tugas['nama_mapel'] }} &bull; {{ $tugas['nama_guru'] }}</p>
</div>
<div class="tugas-right">
<p class="deadline-label">Deadline lewat</p>
<p class="deadline-val">{{ $tugas['deadline']->format('d M Y H:i') }}</p>
<span class="status-badge badge-terlambat">Terlambat</span>
</div>
</a>
@endforeach
</div>
@endif
</div>
{{-- TAB: SELESAI --}}
<div id="tab-selesai" class="tab-panel">
@if($tugasSelesai->isEmpty())
<div class="empty-state">
<div class="empty-icon">📭</div>
<p>Belum ada tugas yang dikumpulkan.</p>
</div>
@else
<div class="tugas-list">
@foreach($tugasSelesai as $tugas)
<a href="{{ route('siswa.tugas.show', $tugas['id_tugas']) }}"
class="tugas-card status-selesai">
<div class="tugas-icon"></div>
<div class="tugas-info">
<p class="tugas-judul">{{ $tugas['judul'] }}</p>
<p class="tugas-meta">{{ $tugas['nama_mapel'] }} &bull; {{ $tugas['nama_guru'] }}</p>
</div>
<div class="tugas-right">
<span class="status-badge badge-selesai">Dikumpulkan</span>
@if($tugas['exp'] > 0)
<br>
<span class="exp-badge"> {{ $tugas['exp'] }} EXP</span>
@endif
</div>
</a>
@endforeach
</div>
@endif
</div>
@endsection
@push('scripts')
<script>
function switchTab(name, btn) {
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
document.getElementById('tab-' + name).classList.add('active');
btn.classList.add('active');
}
</script>
@endpush

View File

@ -0,0 +1,409 @@
@extends('layouts.siswa.app')
@section('title', $tugas->judul_tugas)
@push('styles')
<style>
.back-link {
display: inline-flex;
align-items: center;
gap: 8px;
color: #2b8ef3;
font-weight: 600;
font-size: 14px;
text-decoration: none;
margin-bottom: 20px;
}
.back-link:hover { text-decoration: underline; }
.tugas-header {
background: #fff;
border-radius: 20px;
padding: 28px;
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
margin-bottom: 20px;
border-left: 5px solid #2b8ef3;
}
.tugas-header.terlambat { border-left-color: #ef4444; }
.tugas-header.selesai { border-left-color: #22c55e; }
.tugas-judul {
font-size: 20px;
font-weight: 700;
color: #1e293b;
margin: 0 0 12px;
}
.tugas-meta-row {
display: flex;
flex-wrap: wrap;
gap: 16px;
margin-bottom: 16px;
}
.meta-chip {
display: inline-flex;
align-items: center;
gap: 6px;
background: #f1f5f9;
color: #475569;
font-size: 13px;
font-weight: 500;
padding: 6px 12px;
border-radius: 99px;
}
.meta-chip.deadline-chip {
background: #fee2e2;
color: #ef4444;
}
.meta-chip.deadline-chip.aman {
background: #e6f0ff;
color: #2b8ef3;
}
.keterangan-box {
background: #f8fafc;
border-radius: 12px;
padding: 16px;
font-size: 14px;
color: #475569;
line-height: 1.7;
border: 1px solid #e2e8f0;
margin-top: 12px;
}
/* SUBMIT FORM */
.submit-card {
background: #fff;
border-radius: 20px;
padding: 28px;
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
margin-bottom: 20px;
}
.submit-title {
font-size: 17px;
font-weight: 700;
color: #1e293b;
margin: 0 0 20px;
}
.upload-area {
border: 2px dashed #cbd5e1;
border-radius: 14px;
padding: 32px;
text-align: center;
cursor: pointer;
transition: border-color 0.2s, background 0.2s;
position: relative;
}
.upload-area:hover, .upload-area.drag-over {
border-color: #2b8ef3;
background: #f0f7ff;
}
.upload-area input[type="file"] {
position: absolute;
inset: 0;
opacity: 0;
cursor: pointer;
width: 100%;
height: 100%;
}
.upload-icon { font-size: 36px; margin-bottom: 10px; }
.upload-text {
font-size: 14px;
color: #64748b;
margin: 0 0 4px;
}
.upload-hint {
font-size: 12px;
color: #94a3b8;
margin: 0;
}
.file-preview {
display: none;
align-items: center;
gap: 12px;
background: #f0f7ff;
border-radius: 10px;
padding: 12px 16px;
margin-top: 12px;
}
.file-preview.show { display: flex; }
.file-preview-name {
flex: 1;
font-size: 14px;
font-weight: 600;
color: #1e293b;
}
.file-preview-size {
font-size: 12px;
color: #94a3b8;
}
.btn-submit {
width: 100%;
background: #2b8ef3;
color: white;
border: none;
border-radius: 12px;
padding: 14px;
font-size: 15px;
font-weight: 700;
cursor: pointer;
margin-top: 16px;
transition: background 0.2s;
}
.btn-submit:hover { background: #1a7ae0; }
.btn-submit:disabled {
background: #cbd5e1;
cursor: not-allowed;
}
/* SUDAH KUMPUL */
.sudah-kumpul-card {
background: #dcfce7;
border-radius: 20px;
padding: 28px;
text-align: center;
margin-bottom: 20px;
}
.sudah-kumpul-icon { font-size: 40px; margin-bottom: 10px; }
.sudah-kumpul-title {
font-size: 17px;
font-weight: 700;
color: #166534;
margin: 0 0 8px;
}
.sudah-kumpul-sub {
font-size: 14px;
color: #16a34a;
margin: 0;
}
.exp-display {
display: inline-flex;
align-items: center;
gap: 6px;
background: #fef9c3;
color: #b45309;
font-size: 15px;
font-weight: 700;
padding: 6px 16px;
border-radius: 99px;
margin-top: 12px;
}
.terlambat-warning {
background: #fff7ed;
border: 1px solid #fed7aa;
border-radius: 14px;
padding: 16px 20px;
display: flex;
gap: 12px;
align-items: flex-start;
margin-bottom: 20px;
font-size: 13px;
color: #9a3412;
}
.alert-success {
background: #dcfce7;
color: #166534;
border-radius: 12px;
padding: 14px 18px;
margin-bottom: 20px;
font-weight: 500;
font-size: 14px;
}
.alert-error {
background: #fee2e2;
color: #991b1b;
border-radius: 12px;
padding: 14px 18px;
margin-bottom: 20px;
font-weight: 500;
font-size: 14px;
}
</style>
@endpush
@section('content')
<a href="{{ route('siswa.tugas.index') }}" class="back-link"> Kembali ke Daftar Tugas</a>
@if(session('success'))
<div class="alert-success"> {{ session('success') }}</div>
@endif
@if(session('error'))
<div class="alert-error"> {{ session('error') }}</div>
@endif
{{-- DETAIL TUGAS --}}
@php
$headerClass = $sudahKumpul ? 'selesai' : ($terlambat ? 'terlambat' : '');
$deadlineClass = $terlambat ? '' : 'aman';
@endphp
<div class="tugas-header {{ $headerClass }}">
<h1 class="tugas-judul">{{ $tugas->judul_tugas }}</h1>
<div class="tugas-meta-row">
<span class="meta-chip">
📚 {{ optional(optional($tugas->mengajar)->mapel)->nama_mapel ?? '-' }}
</span>
<span class="meta-chip">
👨‍🏫 {{ optional(optional($tugas->mengajar)->guru)->nama ?? '-' }}
</span>
<span class="meta-chip deadline-chip {{ $deadlineClass }}">
Deadline: {{ \Carbon\Carbon::parse($tugas->deadline)->format('d M Y, H:i') }}
</span>
</div>
@if($tugas->keterangan)
<div class="keterangan-box">
{!! nl2br(e($tugas->keterangan)) !!}
</div>
@endif
</div>
{{-- WARNING TERLAMBAT --}}
@if($terlambat && !$sudahKumpul)
<div class="terlambat-warning">
<span style="font-size:20px">⚠️</span>
<div>
<strong>Deadline sudah lewat!</strong><br>
Kamu masih bisa mengumpulkan tugas ini, tapi akan ditandai sebagai <strong>terlambat</strong>.
</div>
</div>
@endif
{{-- SUDAH DIKUMPULKAN --}}
@if($sudahKumpul)
<div class="sudah-kumpul-card">
<div class="sudah-kumpul-icon">🎉</div>
<p class="sudah-kumpul-title">Tugas sudah dikumpulkan!</p>
<p class="sudah-kumpul-sub">
Dikumpulkan pada {{ \Carbon\Carbon::parse($pengumpulan->tanggal_submit)->format('d M Y, H:i') }}
&bull; Status: <strong>{{ ucfirst($pengumpulan->status) }}</strong>
</p>
@if($pengumpulan->exp > 0)
<div class="exp-display"> {{ $pengumpulan->exp }} EXP didapat</div>
@else
<p style="font-size:13px;color:#16a34a;margin-top:8px">
EXP akan diberikan setelah guru menilai tugasmu.
</p>
@endif
@if($pengumpulan->lampiran_tugas)
<a href="{{ asset('storage/' . $pengumpulan->lampiran_tugas) }}"
target="_blank"
style="display:inline-flex;align-items:center;gap:6px;margin-top:14px;
background:#fff;color:#2b8ef3;padding:8px 18px;border-radius:10px;
font-weight:600;font-size:13px;text-decoration:none;">
📎 Lihat File yang Dikumpulkan
</a>
@endif
</div>
{{-- FORM SUBMIT --}}
@else
<div class="submit-card">
<p class="submit-title">📤 Upload Jawaban</p>
<form action="{{ route('siswa.tugas.submit', $tugas->id_tugas) }}"
method="POST"
enctype="multipart/form-data"
id="submitForm">
@csrf
@error('lampiran_tugas')
<div class="alert-error" style="margin-bottom:16px"> {{ $message }}</div>
@enderror
<div class="upload-area" id="uploadArea">
<input type="file"
name="lampiran_tugas"
id="fileInput"
accept=".pdf,.doc,.docx,.jpg,.jpeg,.png"
required>
<div class="upload-icon">☁️</div>
<p class="upload-text"><strong>Klik untuk pilih file</strong> atau drag & drop di sini</p>
<p class="upload-hint">PDF, DOC, DOCX, JPG, PNG &bull; Maks. 5MB</p>
</div>
<div class="file-preview" id="filePreview">
<span style="font-size:22px">📎</span>
<span class="file-preview-name" id="fileName">-</span>
<span class="file-preview-size" id="fileSize">-</span>
</div>
<button type="submit" class="btn-submit" id="submitBtn" disabled>
Kumpulkan Tugas
</button>
</form>
</div>
@endif
@endsection
@push('scripts')
<script>
const fileInput = document.getElementById('fileInput');
const filePreview = document.getElementById('filePreview');
const fileName = document.getElementById('fileName');
const fileSize = document.getElementById('fileSize');
const submitBtn = document.getElementById('submitBtn');
const uploadArea = document.getElementById('uploadArea');
if (fileInput) {
fileInput.addEventListener('change', function () {
if (this.files && this.files[0]) {
const file = this.files[0];
const size = (file.size / 1024 / 1024).toFixed(2);
fileName.textContent = file.name;
fileSize.textContent = size + ' MB';
filePreview.classList.add('show');
submitBtn.disabled = false;
}
});
// Drag & drop highlight
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('drag-over');
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.classList.remove('drag-over');
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('drag-over');
if (e.dataTransfer.files[0]) {
fileInput.files = e.dataTransfer.files;
fileInput.dispatchEvent(new Event('change'));
}
});
}
</script>
@endpush