siakad-itm/app/Services/Admin/Manage/CourseRegistrationService.php

277 lines
10 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\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(['id', 'user_id', 'student_number', 'department_id'])
->where('status', StudentStatus::Active)
->when($ledDepartmentIds !== null, fn ($q) => $q->whereIn('department_id', $ledDepartmentIds))
->when($departmentId, fn ($q) => $q->where('department_id', $departmentId))
->when($advisorLecturerId, fn ($q) => $q->where('academic_advisor_id', $advisorLecturerId))
->when($search, fn ($q) => $q->where('student_number', 'like', "%{$search}%")
->orWhereHas('user.profile', fn ($q) => $q->where('full_name', 'like', "%{$search}%")))
->with(['user.profile', 'department:id,name'])
->orderBy('student_number')
->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 $allCourseClasses): array
{
$submission = $this->findOrCreateForStudent($student, $activeTerm, $semesterNumber);
$data = $this->withDetails($submission);
$payload = $data->toArray();
$registeredCourseClassIds = $data->courseRegistrations->pluck('course_class_id')->all();
$payload['has_registrations'] = ! empty($registeredCourseClassIds);
$payload['course_registrations'] = $allCourseClasses
->filter(fn (CourseClass $courseClass) => $courseClass->course?->semester_number === $semesterNumber)
->map(fn (CourseClass $courseClass) => [
'id' => $courseClass->id,
'course_class' => ['id' => $courseClass->id, 'course' => $courseClass->course],
'is_registered' => in_array($courseClass->id, $registeredCourseClassIds),
])
->values();
return $payload;
}
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): array
{
if ($semesterNumber === null || ! 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;
});
}
}