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} $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 */ 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 */ 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 */ 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 $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; }); } }