feat: enhance course class management by adding course class assignments and improving course selection UI
This commit is contained in:
parent
28a25fabfd
commit
584056e379
@ -34,6 +34,7 @@ public function index(PaginatedRequest $request): Response
|
||||
'courses' => $this->courseService->getAllForSelect(),
|
||||
'lecturers' => $this->lecturerService->getAllForSelect(),
|
||||
'academicTerms' => $this->academicTermService->getAllForSelect(),
|
||||
'courseClassAssignments' => $this->service->getAllForSelect(),
|
||||
'filters' => $request->only(['academic_term_id', 'method']),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -10,7 +10,13 @@ class CourseService
|
||||
{
|
||||
public function getAllForSelect(): Collection
|
||||
{
|
||||
return Course::select(['id', 'code', 'name', 'department_id'])->get();
|
||||
return Course::query()
|
||||
->select(['courses.id', 'courses.code', 'courses.name', 'courses.department_id', 'courses.semester_number'])
|
||||
->join('departments', 'departments.id', '=', 'courses.department_id')
|
||||
->with('department:id,name')
|
||||
->orderBy('departments.name')
|
||||
->orderBy('courses.semester_number')
|
||||
->get();
|
||||
}
|
||||
|
||||
public function getSemesterNumbers(): array
|
||||
|
||||
@ -112,15 +112,6 @@ function buildNavMain({
|
||||
];
|
||||
|
||||
const kelolaItems: NavItem[] = [
|
||||
...(can('view-course-registrations')
|
||||
? [
|
||||
{
|
||||
name: 'Registrasi KRS',
|
||||
url: courseRegistrationsRoute.url(),
|
||||
icon: FileCheck2,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(can('view-course-classes')
|
||||
? [
|
||||
{
|
||||
@ -130,6 +121,15 @@ function buildNavMain({
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(can('view-course-registrations')
|
||||
? [
|
||||
{
|
||||
name: 'Registrasi KRS',
|
||||
url: courseRegistrationsRoute.url(),
|
||||
icon: FileCheck2,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(can('view-announcements')
|
||||
? [
|
||||
{
|
||||
|
||||
@ -12,10 +12,13 @@ import { PageHeader } from '@/components/page-header';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxCollection,
|
||||
ComboboxContent,
|
||||
ComboboxEmpty,
|
||||
ComboboxGroup,
|
||||
ComboboxInput,
|
||||
ComboboxItem,
|
||||
ComboboxLabel,
|
||||
ComboboxList,
|
||||
} from '@/components/ui/combobox';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@ -39,7 +42,15 @@ import { ClassMethodLabels, ClassMethods } from '@/types/course-class';
|
||||
import type { CourseClass } from '@/types/course-class';
|
||||
import { createCourseClassColumns } from './columns';
|
||||
|
||||
type Course = { id: number; code: string; name: string; department_id: number };
|
||||
type Course = {
|
||||
id: number;
|
||||
code: string;
|
||||
name: string;
|
||||
department_id: number;
|
||||
department: { id: number; name: string } | null;
|
||||
semester_number: number | null;
|
||||
};
|
||||
type CourseGroup = { value: string; items: Course[] };
|
||||
type Lecturer = {
|
||||
id: number;
|
||||
lecturer_number: string;
|
||||
@ -51,6 +62,29 @@ type AcademicTerm = {
|
||||
academic_year: string;
|
||||
semester: string;
|
||||
};
|
||||
type CourseClassAssignment = {
|
||||
id: number;
|
||||
course_id: number;
|
||||
academic_term_id: number;
|
||||
};
|
||||
|
||||
function groupCoursesByDepartmentAndSemester(courses: Course[]): CourseGroup[] {
|
||||
const groups: CourseGroup[] = [];
|
||||
let currentKey: string | null = null;
|
||||
|
||||
for (const course of courses) {
|
||||
const key = `${course.department?.name ?? 'Tanpa Jurusan'} — Semester ${course.semester_number ?? 'Tidak ditentukan'}`;
|
||||
|
||||
if (key !== currentKey) {
|
||||
currentKey = key;
|
||||
groups.push({ value: key, items: [] });
|
||||
}
|
||||
|
||||
groups[groups.length - 1].items.push(course);
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
courseClasses: {
|
||||
@ -63,6 +97,7 @@ type Props = {
|
||||
courses: Course[];
|
||||
lecturers: Lecturer[];
|
||||
academicTerms: AcademicTerm[];
|
||||
courseClassAssignments: CourseClassAssignment[];
|
||||
highlight?: number;
|
||||
filters: {
|
||||
academic_term_id?: string;
|
||||
@ -75,6 +110,7 @@ export default function CourseClassIndex({
|
||||
courses,
|
||||
lecturers,
|
||||
academicTerms,
|
||||
courseClassAssignments,
|
||||
highlight,
|
||||
filters,
|
||||
}: Props) {
|
||||
@ -193,6 +229,7 @@ export default function CourseClassIndex({
|
||||
courses={courses}
|
||||
lecturers={lecturers}
|
||||
academicTerms={academicTerms}
|
||||
courseClassAssignments={courseClassAssignments}
|
||||
/>
|
||||
|
||||
<EditForm
|
||||
@ -207,6 +244,7 @@ export default function CourseClassIndex({
|
||||
courses={courses}
|
||||
lecturers={lecturers}
|
||||
academicTerms={academicTerms}
|
||||
courseClassAssignments={courseClassAssignments}
|
||||
/>
|
||||
|
||||
<DataTable
|
||||
@ -251,28 +289,37 @@ function CreateForm({
|
||||
courses,
|
||||
lecturers,
|
||||
academicTerms,
|
||||
courseClassAssignments,
|
||||
}: {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
courses: Course[];
|
||||
lecturers: Lecturer[];
|
||||
academicTerms: AcademicTerm[];
|
||||
courseClassAssignments: CourseClassAssignment[];
|
||||
}) {
|
||||
const [courseId, setCourseId] = useState('');
|
||||
const [academicTermId, setAcademicTermId] = useState('');
|
||||
const [course, setCourse] = useState<Course | null>(null);
|
||||
const [lecturer, setLecturer] = useState<Lecturer | null>(null);
|
||||
|
||||
const selectedCourse = courses.find((c) => String(c.id) === courseId);
|
||||
const availableLecturers = selectedCourse
|
||||
const takenCourseIds = new Set(
|
||||
courseClassAssignments
|
||||
.filter((a) => String(a.academic_term_id) === academicTermId)
|
||||
.map((a) => a.course_id),
|
||||
);
|
||||
const availableCourses = courses.filter((c) => !takenCourseIds.has(c.id));
|
||||
const courseGroups = groupCoursesByDepartmentAndSemester(availableCourses);
|
||||
const availableLecturers = course
|
||||
? lecturers.filter((l) =>
|
||||
l.departments.some(
|
||||
(department) =>
|
||||
department.id === selectedCourse.department_id,
|
||||
(department) => department.id === course.department_id,
|
||||
),
|
||||
)
|
||||
: lecturers;
|
||||
|
||||
function reset() {
|
||||
setCourseId('');
|
||||
setAcademicTermId('');
|
||||
setCourse(null);
|
||||
setLecturer(null);
|
||||
}
|
||||
|
||||
@ -295,8 +342,19 @@ function CreateForm({
|
||||
Periode Akademik{' '}
|
||||
<span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<input type="hidden" name="academic_term_id" />
|
||||
<Select name="academic_term_id">
|
||||
<input
|
||||
type="hidden"
|
||||
name="academic_term_id"
|
||||
value={academicTermId}
|
||||
/>
|
||||
<Select
|
||||
value={academicTermId}
|
||||
onValueChange={(value) => {
|
||||
setAcademicTermId(value);
|
||||
setCourse(null);
|
||||
setLecturer(null);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Pilih periode akademik" />
|
||||
</SelectTrigger>
|
||||
@ -321,29 +379,50 @@ function CreateForm({
|
||||
<input
|
||||
type="hidden"
|
||||
name="course_id"
|
||||
value={courseId}
|
||||
value={course?.id ?? ''}
|
||||
/>
|
||||
<Select
|
||||
value={courseId}
|
||||
<Combobox
|
||||
items={courseGroups}
|
||||
value={course}
|
||||
onValueChange={(value) => {
|
||||
setCourseId(value);
|
||||
setCourse(value);
|
||||
setLecturer(null);
|
||||
}}
|
||||
itemToStringLabel={(c) => `${c.code} - ${c.name}`}
|
||||
isItemEqualToValue={(a, b) => a.id === b.id}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Pilih mata kuliah" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{courses.map((course) => (
|
||||
<SelectItem
|
||||
key={course.id}
|
||||
value={String(course.id)}
|
||||
>
|
||||
{course.code} - {course.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<ComboboxInput
|
||||
placeholder="Pilih mata kuliah"
|
||||
className="w-full"
|
||||
/>
|
||||
<ComboboxContent>
|
||||
<ComboboxEmpty>
|
||||
Mata kuliah tidak ditemukan.
|
||||
</ComboboxEmpty>
|
||||
<ComboboxList>
|
||||
{(group: CourseGroup) => (
|
||||
<ComboboxGroup
|
||||
key={group.value}
|
||||
items={group.items}
|
||||
>
|
||||
<ComboboxLabel>
|
||||
{group.value}
|
||||
</ComboboxLabel>
|
||||
<ComboboxCollection>
|
||||
{(c: Course) => (
|
||||
<ComboboxItem
|
||||
key={c.id}
|
||||
value={c}
|
||||
>
|
||||
{c.code} - {c.name}
|
||||
</ComboboxItem>
|
||||
)}
|
||||
</ComboboxCollection>
|
||||
</ComboboxGroup>
|
||||
)}
|
||||
</ComboboxList>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
<InputError message={errors.course_id} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
@ -364,12 +443,12 @@ function CreateForm({
|
||||
`${lect.user?.profile?.full_name ?? 'N/A'} - ${lect.lecturer_number}`
|
||||
}
|
||||
isItemEqualToValue={(a, b) => a.id === b.id}
|
||||
disabled={!selectedCourse}
|
||||
disabled={!course}
|
||||
>
|
||||
<ComboboxInput
|
||||
disabled={!selectedCourse}
|
||||
disabled={!course}
|
||||
placeholder={
|
||||
selectedCourse
|
||||
course
|
||||
? 'Pilih dosen pengampu'
|
||||
: 'Pilih mata kuliah terlebih dahulu'
|
||||
}
|
||||
@ -425,6 +504,7 @@ function EditForm({
|
||||
courses,
|
||||
lecturers,
|
||||
academicTerms,
|
||||
courseClassAssignments,
|
||||
}: {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
@ -432,9 +512,15 @@ function EditForm({
|
||||
courses: Course[];
|
||||
lecturers: Lecturer[];
|
||||
academicTerms: AcademicTerm[];
|
||||
courseClassAssignments: CourseClassAssignment[];
|
||||
}) {
|
||||
const [courseId, setCourseId] = useState(
|
||||
editing ? String(editing.course_id) : '',
|
||||
const [academicTermId, setAcademicTermId] = useState(
|
||||
editing ? String(editing.academic_term_id) : '',
|
||||
);
|
||||
const [course, setCourse] = useState<Course | null>(
|
||||
editing
|
||||
? (courses.find((c) => c.id === editing.course_id) ?? null)
|
||||
: null,
|
||||
);
|
||||
const [lecturer, setLecturer] = useState<Lecturer | null>(
|
||||
editing
|
||||
@ -442,12 +528,21 @@ function EditForm({
|
||||
: null,
|
||||
);
|
||||
|
||||
const selectedCourse = courses.find((c) => String(c.id) === courseId);
|
||||
const availableLecturers = selectedCourse
|
||||
const takenCourseIds = new Set(
|
||||
courseClassAssignments
|
||||
.filter(
|
||||
(a) =>
|
||||
String(a.academic_term_id) === academicTermId &&
|
||||
a.id !== editing?.id,
|
||||
)
|
||||
.map((a) => a.course_id),
|
||||
);
|
||||
const availableCourses = courses.filter((c) => !takenCourseIds.has(c.id));
|
||||
const courseGroups = groupCoursesByDepartmentAndSemester(availableCourses);
|
||||
const availableLecturers = course
|
||||
? lecturers.filter((l) =>
|
||||
l.departments.some(
|
||||
(department) =>
|
||||
department.id === selectedCourse.department_id,
|
||||
(department) => department.id === course.department_id,
|
||||
),
|
||||
)
|
||||
: lecturers;
|
||||
@ -472,29 +567,53 @@ function EditForm({
|
||||
<input
|
||||
type="hidden"
|
||||
name="course_id"
|
||||
value={courseId}
|
||||
value={course?.id ?? ''}
|
||||
/>
|
||||
<Select
|
||||
value={courseId}
|
||||
<Combobox
|
||||
items={courseGroups}
|
||||
value={course}
|
||||
onValueChange={(value) => {
|
||||
setCourseId(value);
|
||||
setCourse(value);
|
||||
setLecturer(null);
|
||||
}}
|
||||
itemToStringLabel={(c) =>
|
||||
`${c.code} - ${c.name}`
|
||||
}
|
||||
isItemEqualToValue={(a, b) => a.id === b.id}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Pilih mata kuliah" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{courses.map((course) => (
|
||||
<SelectItem
|
||||
key={course.id}
|
||||
value={String(course.id)}
|
||||
>
|
||||
{course.code} - {course.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<ComboboxInput
|
||||
placeholder="Pilih mata kuliah"
|
||||
className="w-full"
|
||||
/>
|
||||
<ComboboxContent>
|
||||
<ComboboxEmpty>
|
||||
Mata kuliah tidak ditemukan.
|
||||
</ComboboxEmpty>
|
||||
<ComboboxList>
|
||||
{(group: CourseGroup) => (
|
||||
<ComboboxGroup
|
||||
key={group.value}
|
||||
items={group.items}
|
||||
>
|
||||
<ComboboxLabel>
|
||||
{group.value}
|
||||
</ComboboxLabel>
|
||||
<ComboboxCollection>
|
||||
{(c: Course) => (
|
||||
<ComboboxItem
|
||||
key={c.id}
|
||||
value={c}
|
||||
>
|
||||
{c.code} -{' '}
|
||||
{c.name}
|
||||
</ComboboxItem>
|
||||
)}
|
||||
</ComboboxCollection>
|
||||
</ComboboxGroup>
|
||||
)}
|
||||
</ComboboxList>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
<InputError message={errors.course_id} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
@ -515,12 +634,12 @@ function EditForm({
|
||||
`${lect.user?.profile?.full_name ?? 'N/A'} - ${lect.lecturer_number}`
|
||||
}
|
||||
isItemEqualToValue={(a, b) => a.id === b.id}
|
||||
disabled={!selectedCourse}
|
||||
disabled={!course}
|
||||
>
|
||||
<ComboboxInput
|
||||
disabled={!selectedCourse}
|
||||
disabled={!course}
|
||||
placeholder={
|
||||
selectedCourse
|
||||
course
|
||||
? 'Pilih dosen pengampu'
|
||||
: 'Pilih mata kuliah terlebih dahulu'
|
||||
}
|
||||
@ -551,9 +670,18 @@ function EditForm({
|
||||
Periode Akademik{' '}
|
||||
<span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Select
|
||||
<input
|
||||
type="hidden"
|
||||
name="academic_term_id"
|
||||
defaultValue={String(editing.academic_term_id)}
|
||||
value={academicTermId}
|
||||
/>
|
||||
<Select
|
||||
value={academicTermId}
|
||||
onValueChange={(value) => {
|
||||
setAcademicTermId(value);
|
||||
setCourse(null);
|
||||
setLecturer(null);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Pilih periode akademik" />
|
||||
|
||||
Loading…
Reference in New Issue
Block a user