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

View File

@ -6,6 +6,7 @@
use App\Enums\StudentStatus; use App\Enums\StudentStatus;
use App\Models\AcademicTerm; use App\Models\AcademicTerm;
use App\Models\ClassEnrollment; use App\Models\ClassEnrollment;
use App\Models\Course;
use App\Models\CourseClass; use App\Models\CourseClass;
use App\Models\CourseRegistration; use App\Models\CourseRegistration;
use App\Models\CourseRegistrationSubmission; use App\Models\CourseRegistrationSubmission;
@ -85,7 +86,7 @@ public function findOrCreateForSemester(Student $student, string $semester): Cou
/** /**
* @return array<string, mixed> * @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); $submission = $this->findOrCreateForStudent($student, $activeTerm, $semesterNumber);
@ -93,20 +94,41 @@ public function buildSubmissionPayload(Student $student, ?AcademicTerm $activeTe
$payload = $data->toArray(); $payload = $data->toArray();
$registeredCourseClassIds = $data->courseRegistrations->pluck('course_class_id')->all(); $registeredCourseClassIds = $data->courseRegistrations->pluck('course_class_id')->all();
$courseClassesByCourseId = $allCourseClasses->keyBy('course_id');
$payload['has_registrations'] = ! empty($registeredCourseClassIds); $payload['has_registrations'] = ! empty($registeredCourseClassIds);
$payload['course_registrations'] = $allCourseClasses $payload['course_registrations'] = $departmentCourses
->filter(fn (CourseClass $courseClass) => $courseClass->course?->semester_number === $semesterNumber) ->filter(fn (Course $course) => $course->semester_number === $semesterNumber)
->map(fn (CourseClass $courseClass) => [ ->map(function (Course $course) use ($courseClassesByCourseId, $registeredCourseClassIds) {
'id' => $courseClass->id, $courseClass = $courseClassesByCourseId->get($course->id);
'course_class' => ['id' => $courseClass->id, 'course' => $courseClass->course],
'is_registered' => in_array($courseClass->id, $registeredCourseClassIds), 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(); ->values();
return $payload; 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 public function withDetails(CourseRegistrationSubmission $submission): CourseRegistrationSubmission
{ {
return $submission->load([ return $submission->load([

View File

@ -1,6 +1,6 @@
import { Form, Head, Link, router } from '@inertiajs/react'; import { Form, Head, Link, router } from '@inertiajs/react';
import { format } from 'date-fns'; 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 { useState } from 'react';
import { FormDialog } from '@/components/form-dialog'; import { FormDialog } from '@/components/form-dialog';
import InputError from '@/components/input-error'; import InputError from '@/components/input-error';
@ -265,10 +265,10 @@ function SemesterCard({
(sum, registration) => (sum, registration) =>
submission.has_registrations submission.has_registrations
? registration.is_registered ? registration.is_registered
? sum + (registration.course_class?.course?.credits ?? 0) ? sum + registration.course.credits
: sum : sum
: isSemesterOpen : isSemesterOpen && registration.course_class
? sum + (registration.course_class?.course?.credits ?? 0) ? sum + registration.course.credits
: sum, : sum,
0, 0,
); );
@ -373,9 +373,7 @@ function SemesterCard({
index, index,
) => { ) => {
const course = const course =
registration registration.course;
.course_class
?.course;
const checked = const checked =
submission.has_registrations submission.has_registrations
? registration.is_registered ? registration.is_registered
@ -391,34 +389,42 @@ function SemesterCard({
{index + 1} {index + 1}
</TableCell> </TableCell>
<TableCell> <TableCell>
{course?.name ?? {course.name}
'N/A'}
</TableCell> </TableCell>
<TableCell> <TableCell>
{course?.code ?? {course.code}
'-'}
</TableCell> </TableCell>
<TableCell className="text-center"> <TableCell className="text-center">
{course?.credits ?? {
'-'} course.credits
}
</TableCell> </TableCell>
<TableCell className="text-center"> <TableCell className="text-center">
<Checkbox {registration.course_class ? (
name="course_class_ids[]" <Checkbox
value={ name="course_class_ids[]"
registration value={
.course_class registration
?.id .course_class
} .id
defaultChecked={ }
checked defaultChecked={
} checked
disabled={ }
!isSemesterOpen || disabled={
isViewOnly || !isSemesterOpen ||
isLocked isViewOnly ||
} isLocked
/> }
/>
) : (
<span
className="text-xs text-muted-foreground"
title="Kelas belum dibuka untuk periode ini"
>
</span>
)}
</TableCell> </TableCell>
</TableRow> </TableRow>
); );
@ -602,6 +608,7 @@ type Props = {
submissions: Record<string, CourseRegistrationSubmission>; submissions: Record<string, CourseRegistrationSubmission>;
semesters: (number | null)[]; semesters: (number | null)[];
openSemesters: number[]; openSemesters: number[];
hasActiveTerm: boolean;
backHref: string | null; backHref: string | null;
canReview: boolean; canReview: boolean;
canSignAsKaprodi: boolean; canSignAsKaprodi: boolean;
@ -612,6 +619,7 @@ export default function CourseRegistrationDetail({
submissions, submissions,
semesters, semesters,
openSemesters, openSemesters,
hasActiveTerm,
backHref, backHref,
canReview, canReview,
canSignAsKaprodi, canSignAsKaprodi,
@ -670,7 +678,7 @@ export default function CourseRegistrationDetail({
</Tabs> </Tabs>
)} )}
{currentSubmission && ( {currentSubmission ? (
<SemesterCard <SemesterCard
key={currentKey} key={currentKey}
semester={currentSemester} semester={currentSemester}
@ -684,6 +692,40 @@ export default function CourseRegistrationDetail({
canSignAsKaprodi={canSignAsKaprodi} canSignAsKaprodi={canSignAsKaprodi}
canSignAsAdvisor={canSignAsAdvisor} 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> </div>
</> </>

View File

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