siakad-itm/resources/js/pages/student/course-registrations/create.tsx
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

260 lines
11 KiB
TypeScript

import { Form, Head, Link } from '@inertiajs/react';
import { ArrowLeft, XCircle } from 'lucide-react';
import { useState } from 'react';
import InputError from '@/components/input-error';
import { PageHeader } from '@/components/page-header';
import { SignaturePad } from '@/components/signature-pad';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Checkbox } from '@/components/ui/checkbox';
import { index, store } from '@/routes/student/course-registrations';
import type { AcademicTerm } from '@/types/academic-term';
import { formatAcademicTermLabel } from '@/types/academic-term';
type Lecturer = {
user: { profile: { full_name: string } | null } | null;
};
type Course = {
id: number;
code: string;
name: string;
credits: number;
};
type CourseClass = {
id: number;
lecturer: Lecturer | null;
};
type Student = {
student_number: string;
current_semester: number;
department: { name: string } | null;
user: { profile: { full_name: string } | null } | null;
academic_advisor: {
user: { profile: { full_name: string } | null } | null;
} | null;
};
type AvailableCourse = {
course: Course;
course_class: CourseClass | null;
};
type Registration = {
id: number;
course_class: { id: number };
};
type Submission = {
status: 'submitted' | 'approved' | 'rejected';
rejection_reason: string | null;
course_registrations: Registration[];
};
type Props = {
student: Student;
academicTerm: AcademicTerm;
submission: Submission | null;
availableCourses: AvailableCourse[];
};
export default function CourseRegistrationCreate({
student,
academicTerm,
submission,
availableCourses,
}: Props) {
const [selected, setSelected] = useState<number[]>(() => {
if (submission && submission.course_registrations.length > 0) {
return submission.course_registrations.map(
(registration) => registration.course_class.id,
);
}
return availableCourses
.filter((item) => item.course_class)
.map((item) => item.course_class!.id);
});
function toggle(courseClassId: number, checked: boolean) {
setSelected((prev) =>
checked
? [...prev, courseClassId]
: prev.filter((id) => id !== courseClassId),
);
}
const totalCredits = availableCourses
.filter(
(item) =>
item.course_class && selected.includes(item.course_class.id),
)
.reduce((sum, item) => sum + item.course.credits, 0);
return (
<>
<Head title="Isi KRS" />
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-auto p-4 md:p-6">
<PageHeader
title="Isi Kartu Rencana Studi"
actions={
<Button variant="outline" asChild>
<Link href={index.url()}>
<ArrowLeft className="h-4 w-4" />
Kembali
</Link>
</Button>
}
/>
<Card>
<CardHeader>
<CardTitle>
{student.user?.profile?.full_name ?? '-'}
</CardTitle>
<CardDescription>
{student.student_number} &middot;{' '}
{student.department?.name ?? '-'} &middot; Semester{' '}
{student.current_semester}
</CardDescription>
</CardHeader>
<CardContent className="grid gap-1 text-sm text-muted-foreground">
<p>
Periode Akademik:{' '}
{formatAcademicTermLabel(academicTerm)}
</p>
<p>
Dosen Pembimbing Akademik:{' '}
{student.academic_advisor?.user?.profile
?.full_name ?? '-'}
</p>
</CardContent>
</Card>
{submission?.status === 'rejected' && (
<Card className="border-destructive/50">
<CardContent className="pt-6 text-sm">
<p className="flex items-center gap-2 font-medium text-destructive">
<XCircle className="h-4 w-4" />
KRS Anda ditolak
</p>
<p className="mt-1 text-muted-foreground">
{submission.rejection_reason ??
'Tidak ada alasan yang diberikan.'}
</p>
<p className="mt-1 text-muted-foreground">
Silakan perbaiki pilihan mata kuliah Anda dan
ajukan kembali di bawah ini.
</p>
</CardContent>
</Card>
)}
<Form action={store()}>
{({ errors, processing }) => (
<Card>
<CardHeader>
<CardTitle>
Pilih Mata Kuliah Semester{' '}
{student.current_semester}
</CardTitle>
<CardDescription>
Total {totalCredits} SKS dipilih
</CardDescription>
</CardHeader>
<CardContent className="grid gap-3">
{availableCourses.length === 0 && (
<p className="text-sm text-muted-foreground">
Belum ada mata kuliah/kelas untuk
semester ini pada periode aktif.
</p>
)}
{availableCourses.map(
({ course, course_class }) => (
<label
key={course.id}
className="flex items-start gap-3 rounded-md border p-3 has-disabled:opacity-50"
>
<Checkbox
className="mt-0.5"
checked={
course_class
? selected.includes(
course_class.id,
)
: false
}
disabled={!course_class}
onCheckedChange={(checked) =>
course_class &&
toggle(
course_class.id,
checked === true,
)
}
/>
<div>
<p className="font-medium">
{course.code} -{' '}
{course.name}
</p>
<p className="text-xs text-muted-foreground">
{course.credits} SKS
&middot;{' '}
{course_class
? (course_class.lecturer
?.user?.profile
?.full_name ??
'Dosen belum ditentukan')
: 'Kelas belum dibuka'}
</p>
</div>
</label>
),
)}
{selected.map((courseClassId) => (
<input
key={courseClassId}
type="hidden"
name="course_class_ids[]"
value={courseClassId}
/>
))}
<InputError message={errors.course_class_ids} />
</CardContent>
<CardContent className="border-t pt-6">
<SignaturePad
name="signature"
error={errors.signature}
/>
</CardContent>
<CardFooter className="justify-end">
<Button
type="submit"
disabled={
processing || selected.length === 0
}
>
{submission?.status === 'rejected'
? 'Ajukan Ulang KRS'
: 'Tanda Tangan KRS'}
</Button>
</CardFooter>
</Card>
)}
</Form>
</div>
</>
);
}