siakad-itm/app/Services/Admin/Manage/CourseRegistrationService.php
Yoga Pangestu 8336504032
Some checks failed
tests / ci (pull_request) Has been cancelled
feat: add course registration management for admin and student
- Implemented CourseRegistrationShow component for admin to view registration details.
- Added course registration creation and listing for students.
- Enhanced student registration edit and create forms with improved error handling.
- Introduced new columns for course registration submissions in student view.
- Updated routes for course registration management in admin and student contexts.
- Added logging and status handling for course registration submissions.
- Improved type definitions for course registration and user roles.
2026-08-30 20:50:27 +07:00

294 lines
10 KiB
PHP

<?php
namespace App\Services\Admin\Manage;
use App\Enums\RegistrationStatus;
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\Student;
use App\Models\TuitionInvoice;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
class CourseRegistrationService
{
public function paginated(int $perPage = 25, string $search = '', ?string $status = null, ?int $academicTermId = null): LengthAwarePaginator
{
return CourseRegistrationSubmission::query()
->select(['id', 'student_id', 'academic_term_id', 'status', 'signed_at', 'rejection_reason', 'reviewed_by', 'reviewed_at'])
->with([
'student.user.profile',
'student.department',
'academicTerm:id,name,semester,start_date,end_date',
'reviewer.profile',
'courseRegistrations.courseClass.course:id,code,name',
'logs' => fn ($q) => $q->latest(),
'logs.actor.profile',
])
->when($search, fn ($q) => $q->whereHas('student', fn ($q) => $q->where('student_number', 'like', "%{$search}%")
->orWhereHas('user.profile', fn ($q) => $q->where('full_name', 'like', "%{$search}%"))))
->when($status, fn ($q) => $q->where('status', $status))
->when($academicTermId, fn ($q) => $q->where('academic_term_id', $academicTermId))
->latest()
->paginate($perPage);
}
/**
* Admin-created registration: additively attach course classes to a
* student's submission for a term, without disturbing an existing
* submission's review status.
*
* @param array{student_id: int, academic_term_id: int, course_class_ids: array<int, int>} $data
*/
public function create(array $data): CourseRegistrationSubmission
{
return DB::transaction(function () use ($data) {
$submission = CourseRegistrationSubmission::firstOrCreate(
[
'student_id' => $data['student_id'],
'academic_term_id' => $data['academic_term_id'],
],
[
'status' => RegistrationStatus::Submitted,
'signed_at' => now(),
],
);
if ($submission->wasRecentlyCreated) {
$submission->logs()->create([
'status' => RegistrationStatus::Submitted,
'actor_id' => auth()->id(),
]);
}
foreach ($data['course_class_ids'] as $courseClassId) {
CourseRegistration::create([
'student_id' => $data['student_id'],
'academic_term_id' => $data['academic_term_id'],
'course_class_id' => $courseClassId,
'submission_id' => $submission->id,
]);
}
return $submission;
});
}
public function approve(CourseRegistrationSubmission $submission): CourseRegistrationSubmission
{
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
{
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<int, array{course: Course, course_class: ?CourseClass}>
*/
public function availableCourseClasses(Student $student, AcademicTerm $term): Collection
{
$courses = Course::query()
->where('department_id', $student->department_id)
->where('semester_number', $student->current_semester)
->orderBy('name')
->get(['id', 'code', 'name', 'credits']);
$classes = CourseClass::query()
->whereIn('course_id', $courses->pluck('id'))
->where('academic_term_id', $term->id)
->with('lecturer.user.profile')
->get()
->keyBy('course_id');
return $courses->map(fn (Course $course) => [
'course' => $course,
'course_class' => $classes->get($course->id),
])->values();
}
/**
* @return array{has_invoice: bool, is_paid: bool, amount_due: ?float, paid_total: ?float}
*/
public function paymentStatus(Student $student, AcademicTerm $term): array
{
$invoice = TuitionInvoice::query()
->where('student_id', $student->id)
->where('academic_term_id', $term->id)
->withSum('payments as paid_total', 'amount_paid')
->first();
if (! $invoice) {
return [
'has_invoice' => false,
'is_paid' => false,
'amount_due' => null,
'paid_total' => null,
];
}
$paidTotal = (float) ($invoice->paid_total ?? 0);
$amountDue = (float) $invoice->amount_due;
return [
'has_invoice' => true,
'is_paid' => $paidTotal >= $amountDue,
'amount_due' => $amountDue,
'paid_total' => $paidTotal,
];
}
public function currentSubmission(Student $student, AcademicTerm $term): ?CourseRegistrationSubmission
{
return CourseRegistrationSubmission::query()
->where('student_id', $student->id)
->where('academic_term_id', $term->id)
->with([
'courseRegistrations.courseClass.course',
'courseRegistrations.courseClass.lecturer.user.profile',
'logs' => fn ($q) => $q->latest(),
'logs.actor.profile',
])
->first();
}
/**
* @return Collection<int, CourseRegistrationSubmission>
*/
public function submissionsFor(Student $student): Collection
{
return CourseRegistrationSubmission::query()
->where('student_id', $student->id)
->withCount('courseRegistrations')
->with('academicTerm:id,name,semester,start_date,end_date')
->latest()
->get();
}
public function withDetails(CourseRegistrationSubmission $submission): CourseRegistrationSubmission
{
return $submission->load([
'student.user.profile',
'student.department',
'academicTerm:id,name,semester,start_date,end_date',
'reviewer.profile',
'courseRegistrations.courseClass.course',
'courseRegistrations.courseClass.lecturer.user.profile',
'logs' => fn ($q) => $q->oldest(),
'logs.actor.profile',
]);
}
public function canSubmit(?CourseRegistrationSubmission $submission): bool
{
return $submission === null || $submission->status === RegistrationStatus::Rejected;
}
/**
* @return array<int, int>
*/
public function allowedCourseClassIds(Student $student, AcademicTerm $term): array
{
$courseIds = Course::query()
->where('department_id', $student->department_id)
->where('semester_number', $student->current_semester)
->pluck('id');
return CourseClass::query()
->whereIn('course_id', $courseIds)
->where('academic_term_id', $term->id)
->pluck('id')
->all();
}
/**
* Student self-service sign-off: replaces the submission's course
* selection wholesale and attaches the captured signature.
*
* @param array<int, int> $courseClassIds
*/
public function sign(Student $student, AcademicTerm $term, array $courseClassIds, UploadedFile $signatureFile): CourseRegistrationSubmission
{
return DB::transaction(function () use ($student, $term, $courseClassIds, $signatureFile) {
$submission = CourseRegistrationSubmission::updateOrCreate(
['student_id' => $student->id, 'academic_term_id' => $term->id],
[
'status' => RegistrationStatus::Submitted,
'signed_at' => now(),
'rejection_reason' => null,
'reviewed_by' => null,
'reviewed_at' => null,
],
);
$submission->courseRegistrations()->delete();
foreach ($courseClassIds as $courseClassId) {
CourseRegistration::create([
'student_id' => $student->id,
'academic_term_id' => $term->id,
'course_class_id' => $courseClassId,
'submission_id' => $submission->id,
]);
}
$submission->addMedia($signatureFile)->toMediaCollection('signature');
$submission->logs()->create([
'status' => RegistrationStatus::Submitted,
'actor_id' => $student->user_id,
]);
return $submission;
});
}
}