hasRole(UserRole::Kaprodi->value); $advisorLecturerId = ! $isDepartmentLeader && $user->hasRole(UserRole::Dosen->value) ? $user->lecturer?->id : null; $ledDepartmentIds = $isDepartmentLeader ? $user->ledDepartmentIds() : null; $paginator = Student::query() ->select(['students.id', 'students.user_id', 'students.student_number', 'students.department_id']) ->join('departments', 'departments.id', '=', 'students.department_id') ->join('users', 'users.id', '=', 'students.user_id') ->join('user_profiles', 'user_profiles.user_id', '=', 'users.id') ->where('students.status', StudentStatus::Active) ->when($ledDepartmentIds !== null, fn ($q) => $q->whereIn('students.department_id', $ledDepartmentIds)) ->when($departmentId, fn ($q) => $q->where('students.department_id', $departmentId)) ->when($advisorLecturerId, fn ($q) => $q->where('students.academic_advisor_id', $advisorLecturerId)) ->when($search, fn ($q) => $q->where('students.student_number', 'like', "%{$search}%") ->orWhere('user_profiles.full_name', 'like', "%{$search}%")) ->with(['user.profile', 'department:id,name']) ->orderBy('departments.name') ->orderBy('user_profiles.full_name') ->paginate($perPage); return $paginator->through(fn (Student $student) => [ 'student_id' => $student->id, 'student' => $student, ]); } public function findOrCreateForStudent(Student $student, AcademicTerm $academicTerm, ?int $semesterNumber): CourseRegistrationSubmission { return CourseRegistrationSubmission::firstOrCreate( [ 'student_id' => $student->id, 'academic_term_id' => $academicTerm->id, 'semester_number' => $semesterNumber, ], [ 'status' => RegistrationStatus::Submitted, 'signed_at' => now(), ], ); } public function findOrCreateForSemester(Student $student, string $semester): CourseRegistrationSubmission { $semesterNumber = CourseRegistrationSubmission::parseSemesterKey($semester); $activeTerm = $this->academicTermService->getActive(); abort_if($activeTerm === null, 422); return $this->findOrCreateForStudent($student, $activeTerm, $semesterNumber); } /** * @return array */ public function buildSubmissionPayload(Student $student, AcademicTerm $activeTerm, ?int $semesterNumber, Collection $departmentCourses, Collection $allCourseClasses): array { $submission = $this->findOrCreateForStudent($student, $activeTerm, $semesterNumber); $data = $this->withDetails($submission); $payload = $data->toArray(); $registeredCourseClassIds = $data->courseRegistrations->pluck('course_class_id')->all(); $courseClassesByCourseId = $allCourseClasses->keyBy('course_id'); $payload['has_registrations'] = ! empty($registeredCourseClassIds); $payload['course_registrations'] = $departmentCourses ->filter(fn (Course $course) => $course->semester_number === $semesterNumber) ->map(function (Course $course) use ($courseClassesByCourseId, $registeredCourseClassIds) { $courseClass = $courseClassesByCourseId->get($course->id); return [ 'id' => $courseClass?->id ?? -$course->id, 'course' => $course, 'course_class' => $courseClass ? ['id' => $courseClass->id] : null, 'is_registered' => $courseClass !== null && in_array($courseClass->id, $registeredCourseClassIds, true), ]; }) ->values(); return $payload; } /** * The department's full course curriculum, independent of which classes * have been opened for the current academic term. * * @return Collection */ public function departmentCourses(int $departmentId): Collection { return Course::query() ->where('department_id', $departmentId) ->orderBy('semester_number') ->orderBy('name') ->get(['id', 'code', 'name', 'credits', 'semester_number']); } public function withDetails(CourseRegistrationSubmission $submission): CourseRegistrationSubmission { return $submission->load([ 'student.user.profile', 'student.department.currentLeader.lecturer.user.profile', 'student.academicAdvisor.user.profile', 'academicTerm:id,academic_year,semester,start_date,end_date', 'reviewer.profile', 'courseRegistrations.courseClass.course', 'logs' => fn ($q) => $q->oldest(), 'logs.actor.profile', ]); } /** * @return Collection */ public function availableCourseClasses(int $departmentId, int $academicTermId): Collection { return CourseClass::query() ->where('academic_term_id', $academicTermId) ->whereHas('course', fn ($q) => $q->where('department_id', $departmentId)) ->with('course:id,code,name,credits,semester_number') ->get(); } /** * @param array $openSemesters * @return array */ public function selectableCourseClassIds(int $departmentId, int $academicTermId, ?int $semesterNumber, array $openSemesters, ?int $studentCurrentSemester): array { if ($semesterNumber === null || $semesterNumber !== $studentCurrentSemester || ! in_array($semesterNumber, $openSemesters, true)) { return []; } return CourseClass::query() ->where('academic_term_id', $academicTermId) ->whereHas('course', fn ($q) => $q->where('department_id', $departmentId)->where('semester_number', $semesterNumber)) ->pluck('id') ->all(); } /** * @param array $courseClassIds */ public function saveRegistrations( CourseRegistrationSubmission $submission, array $courseClassIds, int $academicTermId, int $studentId, ?UploadedFile $signature, ): CourseRegistrationSubmission { abort_if($submission->status === RegistrationStatus::Approved, 403); return DB::transaction(function () use ($submission, $courseClassIds, $academicTermId, $studentId, $signature) { $submission->courseRegistrations()->delete(); foreach ($courseClassIds as $courseClassId) { CourseRegistration::create([ 'student_id' => $studentId, 'academic_term_id' => $academicTermId, 'course_class_id' => $courseClassId, 'submission_id' => $submission->id, ]); } $submission->update([ 'status' => RegistrationStatus::Submitted, 'rejection_reason' => null, 'signed_at' => now(), ]); if ($signature) { $submission->addMedia($signature)->toMediaCollection('signature'); } $submission->logs()->create([ 'status' => RegistrationStatus::Submitted, 'actor_id' => auth()->id(), ]); return $submission; }); } public function signAsDepartmentLeader(CourseRegistrationSubmission $submission, UploadedFile $signature): CourseRegistrationSubmission { $this->assertSignable($submission); $submission->addMedia($signature)->toMediaCollection('department_leader_signature'); return $submission; } public function signAsAdvisor(CourseRegistrationSubmission $submission, UploadedFile $signature): CourseRegistrationSubmission { $this->assertSignable($submission); $submission->addMedia($signature)->toMediaCollection('advisor_signature'); return $submission; } private function assertSignable(CourseRegistrationSubmission $submission): void { abort_unless($submission->status === RegistrationStatus::Submitted, 403); abort_unless($submission->courseRegistrations()->exists(), 403); } public function approve(CourseRegistrationSubmission $submission): CourseRegistrationSubmission { abort_unless($submission->status === RegistrationStatus::Submitted, 403); abort_if($submission->department_leader_signature_url === null, 422, 'Ketua Program Studi belum menandatangani KRS ini.'); abort_if($submission->advisor_signature_url === null, 422, 'Silakan tanda tangani KRS ini terlebih dahulu.'); return DB::transaction(function () use ($submission) { $submission->update([ 'status' => RegistrationStatus::Approved, 'rejection_reason' => null, 'reviewed_by' => auth()->id(), 'reviewed_at' => now(), ]); $submission->logs()->create([ 'status' => RegistrationStatus::Approved, 'actor_id' => auth()->id(), ]); foreach ($submission->courseRegistrations as $registration) { ClassEnrollment::firstOrCreate( [ 'course_class_id' => $registration->course_class_id, 'student_id' => $registration->student_id, ], [ 'enrolled_at' => now(), ], ); } return $submission; }); } public function reject(CourseRegistrationSubmission $submission, string $reason): CourseRegistrationSubmission { abort_unless($submission->status === RegistrationStatus::Submitted, 403); return DB::transaction(function () use ($submission, $reason) { $submission->update([ 'status' => RegistrationStatus::Rejected, 'rejection_reason' => $reason, 'reviewed_by' => auth()->id(), 'reviewed_at' => now(), ]); $submission->logs()->create([ 'status' => RegistrationStatus::Rejected, 'reason' => $reason, 'actor_id' => auth()->id(), ]); return $submission; }); } /** * @return Collection */ public function departmentSummary(): Collection { return Department::query() ->orderBy('name') ->get(['id', 'name']); } }