Add student achievements module and improve student admin workflows

This commit is contained in:
2026-04-13 06:50:37 +00:00
parent b29c591285
commit fb904f9389
21 changed files with 1111 additions and 180 deletions
@@ -0,0 +1,86 @@
<div class="grid grid-cols-1 gap-5 md:grid-cols-2">
<div class="md:col-span-2">
<label for="student_id_search" class="mb-2 block text-sm font-medium text-slate-700">Siswa</label>
<div class="relative" data-searchable-student-select>
<input
id="student_id_search"
type="text"
value="{{ optional($students->firstWhere('id', old('student_id', $achievement->student_id ?? null)))->full_name ? optional($students->firstWhere('id', old('student_id', $achievement->student_id ?? null)))->full_name . ' — NIS: ' . (optional($students->firstWhere('id', old('student_id', $achievement->student_id ?? null)))->student_number ?: '-') . ' — NISN: ' . (optional($students->firstWhere('id', old('student_id', $achievement->student_id ?? null)))->nisn ?: '-') : '' }}"
class="h-12 w-full rounded-2xl border border-slate-200 bg-white px-4 pr-12 text-sm text-slate-700 placeholder:text-slate-400 focus:border-indigo-300 focus:outline-none focus:ring-4 focus:ring-indigo-100"
placeholder="Ketik nama siswa / NIS / NISN"
autocomplete="off"
data-student-search-input
required
>
<input type="hidden" name="student_id" value="{{ old('student_id', $achievement->student_id ?? '') }}" data-student-search-value>
<button type="button" class="absolute inset-y-0 right-0 flex items-center px-4 text-slate-400 transition hover:text-slate-600" data-student-search-toggle aria-label="Tampilkan daftar siswa">
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m6 9 6 6 6-6" />
</svg>
</button>
<div class="absolute z-30 mt-2 hidden max-h-72 w-full overflow-auto rounded-2xl border border-slate-200 bg-white p-2 shadow-xl shadow-slate-200/60" data-student-search-dropdown>
@foreach ($students as $studentOption)
<button
type="button"
class="flex w-full flex-col rounded-xl px-3 py-3 text-left text-sm transition hover:bg-slate-50"
data-student-option
data-value="{{ $studentOption->id }}"
data-label="{{ $studentOption->full_name }} — NIS: {{ $studentOption->student_number ?: '-' }} — NISN: {{ $studentOption->nisn ?: '-' }}"
data-search="{{ strtolower($studentOption->full_name . ' ' . ($studentOption->student_number ?: '') . ' ' . ($studentOption->nisn ?: '')) }}"
>
<span class="font-semibold text-slate-800">{{ $studentOption->full_name }}</span>
<span class="mt-1 text-xs text-slate-500">NIS: {{ $studentOption->student_number ?: '-' }} · NISN: {{ $studentOption->nisn ?: '-' }}</span>
</button>
@endforeach
<div class="hidden px-3 py-3 text-sm text-slate-500" data-student-empty-state>Tidak ada siswa yang cocok.</div>
</div>
</div>
@error('student_id')
<p class="mt-2 text-sm text-rose-600">{{ $message }}</p>
@enderror
</div>
<x-form-input name="achievement_name" label="Nama Prestasi" :value="old('achievement_name', $achievement->achievement_name ?? '')" required />
<x-form-input name="category" label="Kategori Prestasi" :value="old('category', $achievement->category ?? '')" placeholder="Akademik / Non Akademik / lainnya" />
<x-form-select name="level" label="Tingkat Prestasi" :options="$levelOptions" :value="old('level', $achievement->level ?? '')" placeholder="-- Pilih tingkat --" />
<x-form-input name="rank" label="Peringkat / Predikat" :value="old('rank', $achievement->rank ?? '')" placeholder="Juara 1 / Finalis / Best Speaker" />
<x-form-input name="organizer" label="Penyelenggara" :value="old('organizer', $achievement->organizer ?? '')" placeholder="Nama sekolah / instansi / event" />
<x-form-input name="achievement_year" label="Tahun Prestasi" type="number" :value="old('achievement_year', $achievement->achievement_year ?? '')" placeholder="2026" />
<x-form-input name="achievement_date" label="Tanggal Prestasi" type="date" :value="old('achievement_date', isset($achievement->achievement_date) && $achievement->achievement_date ? $achievement->achievement_date->format('Y-m-d') : '')" />
<div class="md:col-span-2">
<label for="description" class="mb-2 block text-sm font-medium text-slate-700">Keterangan</label>
<textarea id="description" name="description" rows="4" class="w-full rounded-2xl border border-slate-200 bg-white px-4 py-3 text-sm text-slate-700 focus:border-indigo-300 focus:outline-none focus:ring-4 focus:ring-indigo-100" placeholder="Catatan tambahan tentang prestasi siswa">{{ old('description', $achievement->description ?? '') }}</textarea>
@error('description')
<p class="mt-2 text-sm text-rose-600">{{ $message }}</p>
@enderror
</div>
<div class="rounded-3xl border border-slate-200 bg-slate-50 p-5">
<label for="certificate_file" class="mb-2 block text-sm font-medium text-slate-700">Upload Piagam</label>
<div class="flex min-h-[3rem] items-center rounded-2xl border border-slate-200 bg-white px-3 py-2 transition focus-within:border-indigo-300 focus-within:ring-4 focus-within:ring-indigo-100">
<input id="certificate_file" name="certificate_file" type="file" class="block w-full text-sm text-slate-700 file:mr-3 file:rounded-xl file:border-0 file:bg-indigo-50 file:px-4 file:py-2.5 file:text-sm file:font-semibold file:text-indigo-700 hover:file:bg-indigo-100" accept=".pdf,.jpg,.jpeg,.png,.webp">
</div>
<p class="mt-2 text-xs text-slate-500">Format: PDF, JPG, JPEG, PNG, WEBP · maksimal 5 MB.</p>
@if (!empty($achievement->certificate_file))
<p class="mt-2 text-xs text-slate-500">File tersimpan: <a href="{{ asset('storage/' . $achievement->certificate_file) }}" target="_blank" class="font-semibold text-indigo-600 hover:text-indigo-700">Lihat Piagam</a></p>
@endif
@error('certificate_file')
<p class="mt-2 text-sm text-rose-600">{{ $message }}</p>
@enderror
</div>
<div class="rounded-3xl border border-slate-200 bg-slate-50 p-5">
<label for="activity_photo" class="mb-2 block text-sm font-medium text-slate-700">Upload Foto Kegiatan</label>
<div class="flex min-h-[3rem] items-center rounded-2xl border border-slate-200 bg-white px-3 py-2 transition focus-within:border-indigo-300 focus-within:ring-4 focus-within:ring-indigo-100">
<input id="activity_photo" name="activity_photo" type="file" class="block w-full text-sm text-slate-700 file:mr-3 file:rounded-xl file:border-0 file:bg-emerald-50 file:px-4 file:py-2.5 file:text-sm file:font-semibold file:text-emerald-700 hover:file:bg-emerald-100" accept=".jpg,.jpeg,.png,.webp">
</div>
<p class="mt-2 text-xs text-slate-500">Format: JPG, JPEG, PNG, WEBP · maksimal 5 MB.</p>
@if (!empty($achievement->activity_photo))
<p class="mt-2 text-xs text-slate-500">File tersimpan: <a href="{{ asset('storage/' . $achievement->activity_photo) }}" target="_blank" class="font-semibold text-indigo-600 hover:text-indigo-700">Lihat Foto</a></p>
@endif
@error('activity_photo')
<p class="mt-2 text-sm text-rose-600">{{ $message }}</p>
@enderror
</div>
</div>
@@ -0,0 +1,14 @@
<x-master-page title="Tambah Data Siswa Berprestasi" description="Catat prestasi siswa baru ke dalam sistem kesiswaan.">
<x-master-card title="Form Prestasi Siswa" subtitle="Lengkapi data prestasi siswa lalu simpan.">
<form method="POST" action="{{ route('student-achievements.store') }}" enctype="multipart/form-data" class="space-y-6">
@csrf
@include('student-achievements._form')
<div class="flex items-center justify-end gap-3 pt-2">
<a href="{{ route('student-achievements.index') }}" class="rounded-2xl border border-slate-200 px-5 py-3 text-sm font-semibold text-slate-700 transition hover:bg-slate-50">Batal</a>
<button type="submit" class="rounded-2xl bg-slate-900 px-5 py-3 text-sm font-semibold text-white transition hover:bg-slate-800">Simpan</button>
</div>
</form>
</x-master-card>
</x-master-page>
@@ -0,0 +1,15 @@
<x-master-page title="Edit Data Siswa Berprestasi" description="Perbarui data prestasi siswa yang sudah tersimpan.">
<x-master-card title="Form Edit Prestasi Siswa" subtitle="Sesuaikan data prestasi lalu simpan perubahan.">
<form method="POST" action="{{ route('student-achievements.update', $achievement) }}" enctype="multipart/form-data" class="space-y-6">
@csrf
@method('PUT')
@include('student-achievements._form')
<div class="flex items-center justify-end gap-3 pt-2">
<a href="{{ route('student-achievements.index') }}" class="rounded-2xl border border-slate-200 px-5 py-3 text-sm font-semibold text-slate-700 transition hover:bg-slate-50">Batal</a>
<button type="submit" class="rounded-2xl bg-slate-900 px-5 py-3 text-sm font-semibold text-white transition hover:bg-slate-800">Simpan Perubahan</button>
</div>
</form>
</x-master-card>
</x-master-page>
@@ -0,0 +1,98 @@
<x-master-page title="Data Siswa Berprestasi" description="Kelola data prestasi siswa berdasarkan capaian akademik maupun non-akademik." action-label="Tambah Data Prestasi" :action-url="route('student-achievements.create')">
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="rounded-3xl border border-slate-200 bg-white p-6 shadow-sm shadow-slate-200/50">
<div class="text-sm font-medium text-slate-500">Total Prestasi</div>
<div class="mt-3 text-3xl font-bold text-slate-900">{{ $summary['total'] }}</div>
</div>
<div class="rounded-3xl border border-indigo-200 bg-indigo-50 p-6 shadow-sm shadow-indigo-100/50">
<div class="text-sm font-medium text-indigo-700">Hasil Filter</div>
<div class="mt-3 text-3xl font-bold text-indigo-800">{{ $summary['filtered'] }}</div>
</div>
</div>
<x-master-card title="Filter Data Siswa Berprestasi" subtitle="Cari berdasarkan nama siswa, NIS, NISN, kategori, atau tingkat prestasi.">
<form method="GET" action="{{ route('student-achievements.index') }}" class="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4">
<x-form-input name="search" label="Pencarian" :value="$search" placeholder="Nama / NIS / NISN / prestasi" />
<x-form-select name="academic_year_id" label="Tahun Angkatan" :options="$academicYears->all()" :value="$filters['academic_year_id']" />
<x-form-select name="class_id" label="Kelas" :options="$classes->all()" :value="$filters['class_id']" />
<div class="flex items-end gap-3 md:col-span-2 xl:col-span-4">
<button type="submit" class="rounded-2xl bg-slate-900 px-5 py-3 text-sm font-semibold text-white transition hover:bg-slate-800">Cari</button>
<a href="{{ route('student-achievements.index') }}" class="rounded-2xl border border-slate-200 px-5 py-3 text-sm font-semibold text-slate-700 transition hover:bg-slate-50">Reset</a>
</div>
</form>
</x-master-card>
<x-data-table :headers="['Siswa', 'Prestasi', 'Akademik', 'Lampiran', 'Waktu', 'Aksi']">
@forelse ($achievements as $achievement)
<tr>
<td class="px-6 py-4 text-sm text-slate-600">
<div class="font-semibold text-slate-800">{{ $achievement->student?->full_name ?: '-' }}</div>
<div class="mt-1 text-xs text-slate-500">NIS: {{ $achievement->student?->student_number ?: '-' }} · NISN: {{ $achievement->student?->nisn ?: '-' }}</div>
</td>
<td class="px-6 py-4 text-sm text-slate-600">
<div class="font-semibold text-slate-800">{{ $achievement->achievement_name }}</div>
<div class="mt-1 text-xs text-slate-500">
{{ $achievement->category ?: 'Tanpa kategori' }}
@if($achievement->rank)
· {{ $achievement->rank }}
@endif
</div>
<div class="mt-1 text-xs text-slate-500"><span class="font-semibold text-slate-600">Tingkat:</span> {{ $achievement->level ?: '-' }}</div>
<div class="mt-1 text-xs text-slate-400"><span class="font-semibold text-slate-500">Penyelenggara:</span> {{ $achievement->organizer ?: 'Belum diisi' }}</div>
</td>
<td class="px-6 py-4 text-sm text-slate-600">
<div>{{ $achievement->student?->academicYear?->name ?: '-' }}</div>
<div>{{ $achievement->student?->department?->name ?: '-' }}</div>
<div>{{ $achievement->student?->classRoom?->name ?: '-' }}</div>
</td>
<td class="px-6 py-4 text-sm text-slate-600">
<div class="flex flex-col gap-2">
@if($achievement->certificate_file)
@php
$certificateUrl = asset('storage/' . $achievement->certificate_file);
$certificateExtension = strtolower(pathinfo($achievement->certificate_file, PATHINFO_EXTENSION));
$certificateIsImage = in_array($certificateExtension, ['jpg', 'jpeg', 'png', 'webp']);
@endphp
@if($certificateIsImage)
<button type="button" data-lightbox-trigger data-lightbox-src="{{ $certificateUrl }}" data-lightbox-title="Piagam — {{ $achievement->achievement_name }}" class="inline-flex w-fit items-center rounded-xl border border-indigo-200 bg-indigo-50 px-3 py-2 text-xs font-semibold text-indigo-700 transition hover:bg-indigo-100">Lihat Piagam</button>
@else
<a href="{{ $certificateUrl }}" target="_blank" class="inline-flex w-fit items-center rounded-xl border border-indigo-200 bg-indigo-50 px-3 py-2 text-xs font-semibold text-indigo-700 transition hover:bg-indigo-100">Lihat Piagam</a>
@endif
@else
<span class="text-xs text-slate-400">Piagam belum diunggah</span>
@endif
@if($achievement->activity_photo)
<button type="button" data-lightbox-trigger data-lightbox-src="{{ asset('storage/' . $achievement->activity_photo) }}" data-lightbox-title="Foto Kegiatan — {{ $achievement->achievement_name }}" class="inline-flex w-fit items-center rounded-xl border border-emerald-200 bg-emerald-50 px-3 py-2 text-xs font-semibold text-emerald-700 transition hover:bg-emerald-100">Lihat Foto Kegiatan</button>
@else
<span class="text-xs text-slate-400">Foto kegiatan belum diunggah</span>
@endif
</div>
</td>
<td class="px-6 py-4 text-sm text-slate-600">
<div>Tahun: {{ $achievement->achievement_year ?: '-' }}</div>
<div class="mt-1">Tanggal: {{ $achievement->achievement_date?->format('d-m-Y') ?: '-' }}</div>
</td>
<td class="px-6 py-4">
<div class="flex flex-wrap items-center gap-2">
<a href="{{ route('student-achievements.edit', $achievement) }}" class="rounded-xl border border-slate-200 px-3 py-2 text-xs font-semibold text-slate-700 transition hover:bg-slate-50">Edit</a>
<form action="{{ route('student-achievements.destroy', $achievement) }}" method="POST" class="delete-form" data-entity="data prestasi {{ $achievement->achievement_name }}">
@csrf
@method('DELETE')
<button type="submit" class="rounded-xl border border-rose-200 px-3 py-2 text-xs font-semibold text-rose-600 transition hover:bg-rose-50">Hapus</button>
</form>
</div>
</td>
</tr>
@empty
<tr>
<td colspan="6" class="px-6 py-10 text-center text-sm text-slate-500">Belum ada data siswa berprestasi.</td>
</tr>
@endforelse
</x-data-table>
<div>
{{ $achievements->links() }}
</div>
</x-master-page>