303 lines
11 KiB
PHP
303 lines
11 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Admin\Manage;
|
|
|
|
use App\Enums\RegistrationStatus;
|
|
use App\Enums\StudentStatus;
|
|
use App\Models\AcademicTerm;
|
|
use App\Models\ClassEnrollment;
|
|
use App\Models\Course;
|
|
use App\Models\CourseClass;
|
|
use App\Models\CourseRegistration;
|
|
use App\Models\CourseRegistrationSubmission;
|
|
use App\Models\Department;
|
|
use App\Models\Student;
|
|
use App\Services\Admin\Master\AcademicTermService;
|
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class CourseRegistrationService
|
|
{
|
|
public function __construct(
|
|
private readonly AcademicTermService $academicTermService,
|
|
) {}
|
|
|
|
/**
|
|
* @return Collection<int, Department>
|
|
*/
|
|
public function departmentSummary(): Collection
|
|
{
|
|
return Department::query()
|
|
->select('id', 'name')
|
|
->orderBy('name')
|
|
->get();
|
|
}
|
|
|
|
/**
|
|
* @param array<int, int>|null $ledDepartmentIds Restricts results to a kaprodi's own department(s).
|
|
*/
|
|
public function paginated(int $perPage = 25, string $search = '', ?int $departmentId = null, ?int $advisorLecturerId = null, ?array $ledDepartmentIds = null): LengthAwarePaginator
|
|
{
|
|
$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<string, mixed>
|
|
*/
|
|
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<int, Course>
|
|
*/
|
|
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<int, CourseClass>
|
|
*/
|
|
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<int, int> $openSemesters
|
|
* @return array<int, int>
|
|
*/
|
|
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<int, int> $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 signAsKaprodi(CourseRegistrationSubmission $submission, UploadedFile $signature): CourseRegistrationSubmission
|
|
{
|
|
$this->assertSignable($submission);
|
|
|
|
$submission->addMedia($signature)->toMediaCollection('kaprodi_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->kaprodi_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;
|
|
});
|
|
}
|
|
}
|