get('search')); $classes = SchoolClass::with(['academicYear', 'department']) ->withCount(['students', 'academicHistories']) ->when($search !== '', function ($query) use ($search) { $query->where('name', 'like', "%{$search}%") ->orWhere('level', 'like', "%{$search}%") ->orWhere('homeroom_teacher', 'like', "%{$search}%"); }) ->latest() ->paginate(10) ->withQueryString(); return view('classes.index', [ 'classes' => $classes, 'search' => $search, 'academicYears' => AcademicYear::orderByDesc('start_year')->orderByDesc('end_year')->get(), ]); } public function create(): View { return view('classes.create', [ 'academicYears' => AcademicYear::where('is_active', true)->orderBy('name')->pluck('name', 'id'), 'departments' => Department::where('is_active', true)->orderBy('name')->pluck('name', 'id'), ]); } public function store(Request $request): RedirectResponse { $data = $request->validate([ 'academic_year_id' => ['required', Rule::exists('academic_years', 'id')->where(fn ($query) => $query->where('is_active', true))], 'department_id' => ['required', Rule::exists('departments', 'id')->where(fn ($query) => $query->where('is_active', true))], 'name' => ['required', 'string', 'max:100', Rule::unique('classes', 'name')->where(fn ($query) => $query->where('academic_year_id', $request->academic_year_id))], 'level' => ['required', 'string', 'max:10'], 'homeroom_teacher' => ['nullable', 'string', 'max:100'], 'capacity' => ['nullable', 'integer', 'min:1'], 'is_active' => ['nullable', 'boolean'], ]); $data['is_active'] = $request->boolean('is_active'); SchoolClass::create($data); return redirect()->route('classes.index')->with('success', 'Kelas berhasil ditambahkan.'); } public function generate(Request $request): RedirectResponse { $data = $request->validate([ 'source_academic_year_id' => ['required', 'exists:academic_years,id', 'different:target_academic_year_id'], 'target_academic_year_id' => ['required', 'exists:academic_years,id'], 'generation_mode' => ['required', Rule::in(['same_level', 'promote_level'])], ]); $sourceClasses = SchoolClass::query() ->where('academic_year_id', $data['source_academic_year_id']) ->orderBy('department_id') ->orderBy('level') ->orderBy('name') ->get(); if ($sourceClasses->isEmpty()) { Alert::error('Gagal', 'Tidak ada kelas pada tahun ajaran sumber yang bisa digenerate.'); return redirect()->route('classes.index')->withErrors([ 'generate' => 'Tidak ada kelas pada tahun ajaran sumber yang bisa digenerate.', ]); } $created = 0; $skipped = 0; DB::transaction(function () use ($sourceClasses, $data, &$created, &$skipped) { foreach ($sourceClasses as $sourceClass) { $targetLevel = $data['generation_mode'] === 'promote_level' ? $this->nextLevel($sourceClass->level) : $sourceClass->level; if ($targetLevel === null) { $skipped++; continue; } $targetName = $this->transformClassName($sourceClass->name, $sourceClass->level, $targetLevel); $exists = SchoolClass::query() ->where('academic_year_id', $data['target_academic_year_id']) ->where('name', $targetName) ->exists(); if ($exists) { $skipped++; continue; } SchoolClass::create([ 'academic_year_id' => $data['target_academic_year_id'], 'department_id' => $sourceClass->department_id, 'name' => $targetName, 'level' => $targetLevel, 'homeroom_teacher' => $sourceClass->homeroom_teacher, 'capacity' => $sourceClass->capacity, 'is_active' => true, ]); $created++; } }); Alert::success('Berhasil', "Generate kelas selesai. {$created} kelas dibuat, {$skipped} dilewati."); return redirect()->route('classes.index')->with('success', "Generate kelas selesai. {$created} kelas dibuat, {$skipped} dilewati."); } public function show(SchoolClass $class): RedirectResponse { return redirect()->route('classes.edit', $class); } public function edit(SchoolClass $class): View { $class->loadMissing(['academicYear', 'department']); $academicYears = AcademicYear::query() ->where(function ($query) use ($class) { $query->where('is_active', true) ->orWhere('id', $class->academic_year_id); }) ->orderBy('name') ->pluck('name', 'id'); $departments = Department::query() ->where(function ($query) use ($class) { $query->where('is_active', true) ->orWhere('id', $class->department_id); }) ->orderBy('name') ->pluck('name', 'id'); return view('classes.edit', [ 'class' => $class, 'academicYears' => $academicYears, 'departments' => $departments, ]); } public function update(Request $request, SchoolClass $class): RedirectResponse { $data = $request->validate([ 'academic_year_id' => ['required', Rule::exists('academic_years', 'id')->where(fn ($query) => $query->where('is_active', true)->orWhere('id', $class->academic_year_id))], 'department_id' => ['required', Rule::exists('departments', 'id')->where(fn ($query) => $query->where('is_active', true)->orWhere('id', $class->department_id))], 'name' => ['required', 'string', 'max:100', Rule::unique('classes', 'name')->ignore($class->id)->where(fn ($query) => $query->where('academic_year_id', $request->academic_year_id))], 'level' => ['required', 'string', 'max:10'], 'homeroom_teacher' => ['nullable', 'string', 'max:100'], 'capacity' => ['nullable', 'integer', 'min:1'], 'is_active' => ['nullable', 'boolean'], ]); $data['is_active'] = $request->boolean('is_active'); $class->update($data); return redirect()->route('classes.index')->with('success', 'Kelas berhasil diperbarui.'); } public function destroy(SchoolClass $class): RedirectResponse { $class->loadCount(['students', 'academicHistories']); if ($class->students_count > 0) { Alert::error('Tidak bisa dihapus', 'Kelas ini masih digunakan oleh data siswa aktif.'); return redirect()->route('classes.index')->withErrors([ 'delete' => 'Kelas tidak bisa dihapus karena masih digunakan oleh data siswa aktif.', ]); } if ($class->academic_histories_count > 0) { Alert::error('Tidak bisa dihapus', 'Kelas ini masih tercatat pada histori akademik siswa.'); return redirect()->route('classes.index')->withErrors([ 'delete' => 'Kelas tidak bisa dihapus karena masih tercatat pada histori akademik siswa. Hapus atau ubah histori terkait terlebih dahulu.', ]); } Alert::success('Berhasil', 'Kelas berhasil dihapus.'); $class->delete(); 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)) { 'X' => 'XI', 'XI' => 'XII', 'XII' => null, default => $level, }; } protected function transformClassName(string $name, ?string $fromLevel, ?string $toLevel): string { if (! $fromLevel || ! $toLevel || strtoupper($fromLevel) === strtoupper($toLevel)) { return $name; } return preg_replace('/^' . preg_quote($fromLevel, '/') . '\b/i', $toLevel, $name) ?: $name; } }