feat: refactor course registration logic to use department-specific courses and improve submission payload structure

This commit is contained in:
Yoga Pangestu 2026-09-02 13:59:02 +07:00
parent 86863ff2f8
commit 9892f60e5e
4 changed files with 126 additions and 56 deletions

View File

@ -7,7 +7,7 @@
use App\Http\Requests\Admin\Manage\SaveCourseRegistrationRequest;
use App\Http\Requests\Admin\Manage\SignCourseRegistrationRequest;
use App\Http\Requests\PaginatedRequest;
use App\Models\CourseClass;
use App\Models\Course;
use App\Models\CourseRegistrationSubmission;
use App\Models\Student;
use App\Services\Admin\Manage\CourseRegistrationService;
@ -69,25 +69,31 @@ public function show(Student $student): Response
? $this->service->availableCourseClasses($student->department_id, $activeTerm->id)
: collect();
$semesters = $allCourseClasses
->map(fn (CourseClass $courseClass) => $courseClass->course?->semester_number)
$departmentCourses = $this->service->departmentCourses($student->department_id);
$semesters = $departmentCourses
->map(fn (Course $course) => $course->semester_number)
->unique()
->sort()
->values();
$submissions = $semesters->mapWithKeys(fn (?int $semesterNumber) => [
CourseRegistrationSubmission::semesterKey($semesterNumber) => $this->service->buildSubmissionPayload(
$student,
$activeTerm,
$semesterNumber,
$allCourseClasses,
),
]);
$submissions = $activeTerm
? $semesters->mapWithKeys(fn (?int $semesterNumber) => [
CourseRegistrationSubmission::semesterKey($semesterNumber) => $this->service->buildSubmissionPayload(
$student,
$activeTerm,
$semesterNumber,
$departmentCourses,
$allCourseClasses,
),
])
: collect();
return Inertia::render('admin/manage/course-registrations/show', [
'submissions' => $submissions,
'semesters' => $semesters->values(),
'openSemesters' => $activeTerm?->open_semesters ?? [],
'hasActiveTerm' => $activeTerm !== null,
'backHref' => $backHref,
'canReview' => $canReview,
'canSignAsKaprodi' => $canSignAsKaprodi,

View File

@ -6,6 +6,7 @@
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;
@ -85,7 +86,7 @@ public function findOrCreateForSemester(Student $student, string $semester): Cou
/**
* @return array<string, mixed>
*/
public function buildSubmissionPayload(Student $student, ?AcademicTerm $activeTerm, ?int $semesterNumber, Collection $allCourseClasses): array
public function buildSubmissionPayload(Student $student, AcademicTerm $activeTerm, ?int $semesterNumber, Collection $departmentCourses, Collection $allCourseClasses): array
{
$submission = $this->findOrCreateForStudent($student, $activeTerm, $semesterNumber);
@ -93,20 +94,41 @@ public function buildSubmissionPayload(Student $student, ?AcademicTerm $activeTe
$payload = $data->toArray();
$registeredCourseClassIds = $data->courseRegistrations->pluck('course_class_id')->all();
$courseClassesByCourseId = $allCourseClasses->keyBy('course_id');
$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),
])
$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([

View File

@ -1,6 +1,6 @@
import { Form, Head, Link, router } from '@inertiajs/react';
import { format } from 'date-fns';
import { ArrowLeft, Check, Send, X } from 'lucide-react';
import { ArrowLeft, Check, Inbox, Send, X } from 'lucide-react';
import { useState } from 'react';
import { FormDialog } from '@/components/form-dialog';
import InputError from '@/components/input-error';
@ -265,10 +265,10 @@ function SemesterCard({
(sum, registration) =>
submission.has_registrations
? registration.is_registered
? sum + (registration.course_class?.course?.credits ?? 0)
? sum + registration.course.credits
: sum
: isSemesterOpen
? sum + (registration.course_class?.course?.credits ?? 0)
: isSemesterOpen && registration.course_class
? sum + registration.course.credits
: sum,
0,
);
@ -373,9 +373,7 @@ function SemesterCard({
index,
) => {
const course =
registration
.course_class
?.course;
registration.course;
const checked =
submission.has_registrations
? registration.is_registered
@ -391,34 +389,42 @@ function SemesterCard({
{index + 1}
</TableCell>
<TableCell>
{course?.name ??
'N/A'}
{course.name}
</TableCell>
<TableCell>
{course?.code ??
'-'}
{course.code}
</TableCell>
<TableCell className="text-center">
{course?.credits ??
'-'}
{
course.credits
}
</TableCell>
<TableCell className="text-center">
<Checkbox
name="course_class_ids[]"
value={
registration
.course_class
?.id
}
defaultChecked={
checked
}
disabled={
!isSemesterOpen ||
isViewOnly ||
isLocked
}
/>
{registration.course_class ? (
<Checkbox
name="course_class_ids[]"
value={
registration
.course_class
.id
}
defaultChecked={
checked
}
disabled={
!isSemesterOpen ||
isViewOnly ||
isLocked
}
/>
) : (
<span
className="text-xs text-muted-foreground"
title="Kelas belum dibuka untuk periode ini"
>
</span>
)}
</TableCell>
</TableRow>
);
@ -602,6 +608,7 @@ type Props = {
submissions: Record<string, CourseRegistrationSubmission>;
semesters: (number | null)[];
openSemesters: number[];
hasActiveTerm: boolean;
backHref: string | null;
canReview: boolean;
canSignAsKaprodi: boolean;
@ -612,6 +619,7 @@ export default function CourseRegistrationDetail({
submissions,
semesters,
openSemesters,
hasActiveTerm,
backHref,
canReview,
canSignAsKaprodi,
@ -670,7 +678,7 @@ export default function CourseRegistrationDetail({
</Tabs>
)}
{currentSubmission && (
{currentSubmission ? (
<SemesterCard
key={currentKey}
semester={currentSemester}
@ -684,6 +692,40 @@ export default function CourseRegistrationDetail({
canSignAsKaprodi={canSignAsKaprodi}
canSignAsAdvisor={canSignAsAdvisor}
/>
) : (
semesters.length > 0 &&
!hasActiveTerm && (
<Card>
<CardContent className="flex flex-col items-center gap-2 py-12 text-center">
<Inbox className="h-10 w-10 text-muted-foreground" />
<p className="font-medium">
Belum ada periode akademik aktif
</p>
<p className="max-w-sm text-sm text-muted-foreground">
Registrasi KRS hanya dapat dilakukan saat
ada periode akademik yang aktif. Silakan
hubungi admin untuk informasi lebih
lanjut.
</p>
</CardContent>
</Card>
)
)}
{semesters.length === 0 && (
<Card>
<CardContent className="flex flex-col items-center gap-2 py-12 text-center">
<Inbox className="h-10 w-10 text-muted-foreground" />
<p className="font-medium">
Belum ada mata kuliah terdaftar
</p>
<p className="max-w-sm text-sm text-muted-foreground">
Belum ada mata kuliah yang terdaftar untuk
jurusan ini. Silakan hubungi admin atau
program studi.
</p>
</CardContent>
</Card>
)}
</div>
</>

View File

@ -52,15 +52,15 @@ export type CourseRegistrationSubmissionStudent = {
export type CourseRegistrationSubmissionEntry = {
id: number;
is_registered: boolean;
course: {
id: number;
code: string;
name: string;
credits: number;
semester_number: number | null;
};
course_class: {
id: number;
course: {
id: number;
code: string;
name: string;
credits: number;
semester_number: number | null;
} | null;
} | null;
};