Some checks failed
tests / ci (pull_request) Has been cancelled
- 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.
294 lines
10 KiB
PHP
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;
|
|
});
|
|
}
|
|
}
|