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