Add student achievements module and improve student admin workflows
This commit is contained in:
@@ -7,6 +7,7 @@ use App\Models\Department;
|
||||
use App\Models\DocumentType;
|
||||
use App\Models\SchoolClass;
|
||||
use App\Models\Student;
|
||||
use App\Models\StudentAchievement;
|
||||
use App\Models\StudentExit;
|
||||
use App\Models\StudentTransfer;
|
||||
use Illuminate\Support\Collection;
|
||||
@@ -24,6 +25,23 @@ class DashboardController extends Controller
|
||||
|
||||
$completion = $this->calculateCompletion($students, $requiredDocumentTypeIds);
|
||||
|
||||
$studentGenderByAcademicYear = AcademicYear::query()
|
||||
->orderByDesc('name')
|
||||
->get(['id', 'name'])
|
||||
->map(function (AcademicYear $academicYear) {
|
||||
$maleCount = Student::where('academic_year_id', $academicYear->id)->where('gender', 'L')->count();
|
||||
$femaleCount = Student::where('academic_year_id', $academicYear->id)->where('gender', 'P')->count();
|
||||
|
||||
return [
|
||||
'name' => $academicYear->name,
|
||||
'male' => $maleCount,
|
||||
'female' => $femaleCount,
|
||||
'total' => $maleCount + $femaleCount,
|
||||
];
|
||||
})
|
||||
->filter(fn (array $item) => $item['total'] > 0)
|
||||
->values();
|
||||
|
||||
return view('dashboard', [
|
||||
'stats' => [
|
||||
'academicYears' => AcademicYear::count(),
|
||||
@@ -34,11 +52,14 @@ class DashboardController extends Controller
|
||||
'studentsDropped' => StudentExit::count(),
|
||||
'studentsTransferred' => StudentTransfer::count(),
|
||||
'studentsGraduated' => Student::where('status', 'graduated')->count(),
|
||||
'studentAchievements' => StudentAchievement::count(),
|
||||
'studentsWithAchievements' => StudentAchievement::distinct('student_id')->count('student_id'),
|
||||
'studentsCompleteDocuments' => $completion['complete'],
|
||||
'studentsIncompleteDocuments' => $completion['incomplete'],
|
||||
'studentsVerifiedDocuments' => $completion['verified_complete'],
|
||||
'studentsUnverifiedDocuments' => $completion['verified_incomplete'],
|
||||
],
|
||||
'studentGenderByAcademicYear' => $studentGenderByAcademicYear,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ class PublicStudentLookupController extends Controller
|
||||
'department',
|
||||
'classRoom',
|
||||
'documents.documentType',
|
||||
'achievements',
|
||||
])
|
||||
->where(function ($builder) use ($query) {
|
||||
$builder->where('student_number', 'like', "%{$query}%")
|
||||
|
||||
@@ -209,6 +209,41 @@ class SchoolClassController extends Controller
|
||||
return redirect()->route('classes.index')->with('success', 'Kelas berhasil dihapus.');
|
||||
}
|
||||
|
||||
public function bulkDestroy(Request $request): RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'class_ids' => ['required', 'array', 'min:1'],
|
||||
'class_ids.*' => ['integer', 'exists:classes,id'],
|
||||
], [
|
||||
'class_ids.required' => 'Pilih minimal satu kelas untuk dihapus.',
|
||||
]);
|
||||
|
||||
$classes = SchoolClass::withCount(['students', 'academicHistories'])
|
||||
->whereIn('id', $validated['class_ids'])
|
||||
->get();
|
||||
|
||||
$blocked = $classes->filter(fn (SchoolClass $class) => $class->students_count > 0 || $class->academic_histories_count > 0);
|
||||
|
||||
if ($blocked->isNotEmpty()) {
|
||||
Alert::error('Tidak bisa dihapus', 'Sebagian kelas masih dipakai oleh siswa aktif atau histori akademik.');
|
||||
|
||||
return redirect()->route('classes.index')->withErrors([
|
||||
'delete' => 'Beberapa kelas tidak bisa dihapus karena masih dipakai oleh data siswa atau histori akademik.',
|
||||
]);
|
||||
}
|
||||
|
||||
$deletedCount = 0;
|
||||
|
||||
foreach ($classes as $class) {
|
||||
$class->delete();
|
||||
$deletedCount++;
|
||||
}
|
||||
|
||||
Alert::success('Berhasil', $deletedCount . ' kelas berhasil dihapus.');
|
||||
|
||||
return redirect()->route('classes.index')->with('success', $deletedCount . ' kelas berhasil dihapus.');
|
||||
}
|
||||
|
||||
protected function nextLevel(?string $level): ?string
|
||||
{
|
||||
return match (strtoupper((string) $level)) {
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\AcademicYear;
|
||||
use App\Models\SchoolClass;
|
||||
use App\Models\Student;
|
||||
use App\Models\StudentAchievement;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class StudentAchievementController extends Controller
|
||||
{
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$search = trim((string) $request->get('search'));
|
||||
$academicYearId = $request->get('academic_year_id');
|
||||
$classId = $request->get('class_id');
|
||||
|
||||
$achievements = StudentAchievement::query()
|
||||
->with(['student.academicYear', 'student.department', 'student.classRoom'])
|
||||
->when($search !== '', function ($query) use ($search) {
|
||||
$query->where(function ($subQuery) use ($search) {
|
||||
$subQuery->where('achievement_name', 'like', "%{$search}%")
|
||||
->orWhere('category', 'like', "%{$search}%")
|
||||
->orWhere('level', 'like', "%{$search}%")
|
||||
->orWhereHas('student', function ($studentQuery) use ($search) {
|
||||
$studentQuery->where('full_name', 'like', "%{$search}%")
|
||||
->orWhere('student_number', 'like', "%{$search}%")
|
||||
->orWhere('nisn', 'like', "%{$search}%");
|
||||
});
|
||||
});
|
||||
})
|
||||
->when($academicYearId, fn ($query) => $query->whereHas('student', fn ($studentQuery) => $studentQuery->where('academic_year_id', $academicYearId)))
|
||||
->when($classId, fn ($query) => $query->whereHas('student', fn ($studentQuery) => $studentQuery->where('class_id', $classId)))
|
||||
->latest('achievement_year')
|
||||
->latest('achievement_date')
|
||||
->latest('id')
|
||||
->paginate(15)
|
||||
->withQueryString();
|
||||
|
||||
return view('student-achievements.index', [
|
||||
'achievements' => $achievements,
|
||||
'search' => $search,
|
||||
'filters' => [
|
||||
'academic_year_id' => $academicYearId,
|
||||
'class_id' => $classId,
|
||||
],
|
||||
'academicYears' => AcademicYear::orderBy('name')->pluck('name', 'id'),
|
||||
'classes' => SchoolClass::orderBy('name')->pluck('name', 'id'),
|
||||
'summary' => [
|
||||
'total' => StudentAchievement::count(),
|
||||
'filtered' => $achievements->total(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(Request $request): View
|
||||
{
|
||||
$achievement = new StudentAchievement();
|
||||
|
||||
if ($request->filled('student_id')) {
|
||||
$achievement->student_id = (int) $request->integer('student_id');
|
||||
}
|
||||
|
||||
return view('student-achievements.create', [
|
||||
'achievement' => $achievement,
|
||||
'students' => Student::orderBy('full_name')->get(['id', 'full_name', 'student_number', 'nisn']),
|
||||
'levelOptions' => $this->levelOptions(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$data = $this->validateAchievement($request);
|
||||
|
||||
if ($request->hasFile('certificate_file')) {
|
||||
$data['certificate_file'] = $request->file('certificate_file')->store('student-achievements/certificates', 'public');
|
||||
}
|
||||
|
||||
if ($request->hasFile('activity_photo')) {
|
||||
$data['activity_photo'] = $request->file('activity_photo')->store('student-achievements/activity-photos', 'public');
|
||||
}
|
||||
|
||||
StudentAchievement::create($data);
|
||||
|
||||
return redirect()->route('student-achievements.index')->with('success', 'Data siswa berprestasi berhasil ditambahkan.');
|
||||
}
|
||||
|
||||
public function edit(StudentAchievement $studentAchievement): View
|
||||
{
|
||||
return view('student-achievements.edit', [
|
||||
'achievement' => $studentAchievement,
|
||||
'students' => Student::orderBy('full_name')->get(['id', 'full_name', 'student_number', 'nisn']),
|
||||
'levelOptions' => $this->levelOptions(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request, StudentAchievement $studentAchievement): RedirectResponse
|
||||
{
|
||||
$data = $this->validateAchievement($request);
|
||||
|
||||
if ($request->hasFile('certificate_file')) {
|
||||
if ($studentAchievement->certificate_file && Storage::disk('public')->exists($studentAchievement->certificate_file)) {
|
||||
Storage::disk('public')->delete($studentAchievement->certificate_file);
|
||||
}
|
||||
|
||||
$data['certificate_file'] = $request->file('certificate_file')->store('student-achievements/certificates', 'public');
|
||||
}
|
||||
|
||||
if ($request->hasFile('activity_photo')) {
|
||||
if ($studentAchievement->activity_photo && Storage::disk('public')->exists($studentAchievement->activity_photo)) {
|
||||
Storage::disk('public')->delete($studentAchievement->activity_photo);
|
||||
}
|
||||
|
||||
$data['activity_photo'] = $request->file('activity_photo')->store('student-achievements/activity-photos', 'public');
|
||||
}
|
||||
|
||||
$studentAchievement->update($data);
|
||||
|
||||
return redirect()->route('student-achievements.index')->with('success', 'Data siswa berprestasi berhasil diperbarui.');
|
||||
}
|
||||
|
||||
public function destroy(StudentAchievement $studentAchievement): RedirectResponse
|
||||
{
|
||||
if ($studentAchievement->certificate_file && Storage::disk('public')->exists($studentAchievement->certificate_file)) {
|
||||
Storage::disk('public')->delete($studentAchievement->certificate_file);
|
||||
}
|
||||
|
||||
if ($studentAchievement->activity_photo && Storage::disk('public')->exists($studentAchievement->activity_photo)) {
|
||||
Storage::disk('public')->delete($studentAchievement->activity_photo);
|
||||
}
|
||||
|
||||
$studentAchievement->delete();
|
||||
|
||||
return redirect()->route('student-achievements.index')->with('success', 'Data siswa berprestasi berhasil dihapus.');
|
||||
}
|
||||
|
||||
protected function validateAchievement(Request $request): array
|
||||
{
|
||||
return $request->validate([
|
||||
'student_id' => ['required', 'exists:students,id'],
|
||||
'achievement_name' => ['required', 'string', 'max:150'],
|
||||
'category' => ['nullable', 'string', 'max:100'],
|
||||
'level' => ['nullable', 'string', 'max:50'],
|
||||
'organizer' => ['nullable', 'string', 'max:150'],
|
||||
'rank' => ['nullable', 'string', 'max:100'],
|
||||
'achievement_date' => ['nullable', 'date'],
|
||||
'achievement_year' => ['nullable', 'integer', 'min:2000', 'max:' . now()->year],
|
||||
'description' => ['nullable', 'string'],
|
||||
'certificate_file' => ['nullable', 'file', 'mimes:pdf,jpg,jpeg,png,webp', 'max:5120'],
|
||||
'activity_photo' => ['nullable', 'image', 'mimes:jpg,jpeg,png,webp', 'max:5120'],
|
||||
]);
|
||||
}
|
||||
|
||||
protected function levelOptions(): array
|
||||
{
|
||||
return [
|
||||
'Sekolah' => 'Sekolah',
|
||||
'Kecamatan' => 'Kecamatan',
|
||||
'Kabupaten/Kota' => 'Kabupaten/Kota',
|
||||
'Provinsi' => 'Provinsi',
|
||||
'Nasional' => 'Nasional',
|
||||
'Internasional' => 'Internasional',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -65,7 +65,7 @@ class StudentController extends Controller
|
||||
|
||||
$students = Student::query()
|
||||
->select('students.*')
|
||||
->with(['academicYear', 'department', 'classRoom', 'documents.documentType'])
|
||||
->with(['academicYear', 'department', 'classRoom', 'documents.documentType', 'achievements'])
|
||||
->leftJoin('academic_years', 'academic_years.id', '=', 'students.academic_year_id')
|
||||
->leftJoin('departments', 'departments.id', '=', 'students.department_id')
|
||||
->leftJoin('classes', 'classes.id', '=', 'students.class_id')
|
||||
@@ -128,6 +128,25 @@ class StudentController extends Controller
|
||||
: 100;
|
||||
$student->hover_photo_url = $photoDocument?->file_path ? asset('storage/' . $photoDocument->file_path) : null;
|
||||
|
||||
$achievementPriority = [
|
||||
'Internasional' => 6,
|
||||
'Nasional' => 5,
|
||||
'Provinsi' => 4,
|
||||
'Kabupaten/Kota' => 3,
|
||||
'Kecamatan' => 2,
|
||||
'Sekolah' => 1,
|
||||
];
|
||||
|
||||
$topAchievement = $student->achievements
|
||||
->sortByDesc(fn ($achievement) => $achievementPriority[$achievement->level] ?? 0)
|
||||
->first();
|
||||
|
||||
$student->achievement_badge_level = $topAchievement?->level;
|
||||
$student->achievement_badge_count = $student->achievements->count();
|
||||
$student->achievement_badge_title = $topAchievement
|
||||
? sprintf('Siswa berprestasi · %s · %s prestasi', $topAchievement->level ?: 'Prestasi', $student->achievement_badge_count)
|
||||
: null;
|
||||
|
||||
return $student;
|
||||
});
|
||||
|
||||
@@ -177,7 +196,7 @@ class StudentController extends Controller
|
||||
|
||||
public function show(Student $student): View
|
||||
{
|
||||
$student->load(['academicYear', 'department', 'classRoom', 'parents', 'documents.documentType', 'academicHistories.academicYear', 'academicHistories.department', 'academicHistories.classRoom']);
|
||||
$student->load(['academicYear', 'department', 'classRoom', 'parents', 'documents.documentType', 'academicHistories.academicYear', 'academicHistories.department', 'academicHistories.classRoom', 'achievements']);
|
||||
|
||||
$requiredDocumentTypes = DocumentType::where('is_active', true)
|
||||
->where('is_required', true)
|
||||
|
||||
@@ -144,4 +144,9 @@ class Student extends Model
|
||||
{
|
||||
return $this->hasOne(StudentTransfer::class);
|
||||
}
|
||||
|
||||
public function achievements(): HasMany
|
||||
{
|
||||
return $this->hasMany(StudentAchievement::class)->latest('achievement_year')->latest('achievement_date');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class StudentAchievement extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'student_id',
|
||||
'achievement_name',
|
||||
'category',
|
||||
'level',
|
||||
'organizer',
|
||||
'rank',
|
||||
'achievement_date',
|
||||
'achievement_year',
|
||||
'description',
|
||||
'certificate_file',
|
||||
'activity_photo',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'achievement_date' => 'date',
|
||||
'achievement_year' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
public function student(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Student::class);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user