import { Head, router } from '@inertiajs/react'; import { Copy, Plus } from 'lucide-react'; import { useState } from 'react'; import type { PaginationState } from '@/components/data-table'; import { DataTable } from '@/components/data-table'; import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog'; import type { FilterField } from '@/components/filter-dialog'; import { FilterDialog } from '@/components/filter-dialog'; import { FormDialog } from '@/components/form-dialog'; import InputError from '@/components/input-error'; import { PageHeader } from '@/components/page-header'; import { Button } from '@/components/ui/button'; import { Combobox, ComboboxChip, ComboboxChips, ComboboxChipsInput, ComboboxCollection, ComboboxContent, ComboboxEmpty, ComboboxGroup, ComboboxInput, ComboboxItem, ComboboxLabel, ComboboxList, useComboboxAnchor, } from '@/components/ui/combobox'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { usePermissions } from '@/hooks/use-permissions'; import { useServerTable } from '@/hooks/use-server-table'; import { index as courseClassIndex, destroy, duplicate, store, update, } from '@/routes/admin/manage/course-classes'; import { formatAcademicTermLabel } from '@/types/academic-term'; 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; department: { id: number; name: string } | null; semester_number: number | null; }; type CourseGroup = { value: string; items: Course[] }; type Lecturer = { id: number; lecturer_number: string; departments: { id: number; name: string }[]; user: { profile: { full_name: string } | null } | null; }; type AcademicTerm = { id: number; academic_year: string; semester: string; }; type CourseClassAssignment = { id: number; course_id: number; academic_term_id: number; }; function courseMatchesTermParity( course: Course, term: AcademicTerm | undefined, ): boolean { if (!term || course.semester_number === null) { return true; } const isOddSemester = course.semester_number % 2 === 1; return term.semester === 'odd' ? isOddSemester : !isOddSemester; } 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: { data: CourseClass[]; current_page: number; last_page: number; per_page: number; total: number; }; courses: Course[]; lecturers: Lecturer[]; academicTerms: AcademicTerm[]; courseClassAssignments: CourseClassAssignment[]; highlight?: number; filters: { academic_term_id?: string; method?: string; }; }; export default function CourseClassIndex({ courseClasses, courses, lecturers, academicTerms, courseClassAssignments, highlight, filters, }: Props) { const [createOpen, setCreateOpen] = useState(false); const [duplicateOpen, setDuplicateOpen] = useState(false); const [editing, setEditing] = useState(null); const [deleting, setDeleting] = useState(null); const { hasPermission } = usePermissions(); const canCreate = hasPermission('create-course-classes'); const canUpdate = hasPermission('update-course-classes'); const canDelete = hasPermission('delete-course-classes'); const canViewEnrollments = hasPermission('view-course-class-enrollments'); const filterFields: FilterField[] = [ { key: 'academic_term_id', label: 'Periode Akademik', options: academicTerms.map((term) => ({ value: String(term.id), label: formatAcademicTermLabel(term), })), }, { key: 'method', label: 'Metode', options: ClassMethods.map((method) => ({ value: method, label: ClassMethodLabels[method], })), }, ]; const pagination: PaginationState = { current_page: courseClasses.current_page, last_page: courseClasses.last_page, per_page: courseClasses.per_page, total: courseClasses.total, }; const { search, handlePageChange, handlePerPageChange, handleSearchChange, applyFilters, } = useServerTable({ route: () => courseClassIndex.url(), pagination, filters, }); function handleDelete() { if (!deleting) { return; } router.delete(destroy(deleting.id), { onSuccess: () => setDeleting(null), }); } const columns = createCourseClassColumns({ handleEdit: (courseClass) => setEditing(courseClass), handleDeleteClick: (courseClass) => setDeleting(courseClass), canUpdate, canDelete, canViewEnrollments, }); return ( <>
Menampilkan kelas mata kuliah dari notifikasi.

) } actions={ canCreate && (
) } /> { if (!open) { setEditing(null); } }} editing={editing} courses={courses} lecturers={lecturers} academicTerms={academicTerms} courseClassAssignments={courseClassAssignments} /> `${courseClass.academic_term ? formatAcademicTermLabel(courseClass.academic_term) : 'Tanpa Periode'} — Semester ${courseClass.course?.semester_number ?? 'Tidak ditentukan'}` } toolbar={ } /> { if (!open) { setDeleting(null); } }} title="Hapus Kelas Mata Kuliah" description={(courseClass) => `Apakah Anda yakin ingin menghapus kelas "${courseClass.course?.name}"? Tindakan ini tidak dapat dibatalkan.` } onConfirm={handleDelete} />
); } function CreateForm({ open, onOpenChange, courses, lecturers, academicTerms, courseClassAssignments, }: { open: boolean; onOpenChange: (open: boolean) => void; courses: Course[]; lecturers: Lecturer[]; academicTerms: AcademicTerm[]; courseClassAssignments: CourseClassAssignment[]; }) { const [academicTermId, setAcademicTermId] = useState(''); const [selectedCourses, setSelectedCourses] = useState([]); const [lecturer, setLecturer] = useState(null); const courseAnchor = useComboboxAnchor(); const selectedTerm = academicTerms.find( (term) => String(term.id) === academicTermId, ); const takenCourseIds = new Set( courseClassAssignments .filter((a) => String(a.academic_term_id) === academicTermId) .map((a) => a.course_id), ); const selectedCourseIds = new Set(selectedCourses.map((c) => c.id)); const lecturerDepartmentIds = new Set( lecturer ? lecturer.departments.map((d) => d.id) : [], ); const availableCourses = lecturer ? courses.filter( (c) => lecturerDepartmentIds.has(c.department_id) && !takenCourseIds.has(c.id) && !selectedCourseIds.has(c.id) && courseMatchesTermParity(c, selectedTerm), ) : []; const courseGroups = groupCoursesByDepartmentAndSemester(availableCourses); function reset() { setAcademicTermId(''); setSelectedCourses([]); setLecturer(null); } return ( { onOpenChange(false); reset(); }} > {({ errors }) => (
{ setLecturer(value); setSelectedCourses([]); }} itemToStringLabel={(lect) => `${lect.user?.profile?.full_name ?? 'N/A'} - ${lect.lecturer_number}` } isItemEqualToValue={(a, b) => a.id === b.id} > Dosen tidak ditemukan. {(lect: Lecturer) => ( {lect.user?.profile?.full_name ?? 'N/A'}{' '} - {lect.lecturer_number} )}
{selectedCourses.map((c) => ( ))} `${c.code} - ${c.name}`} isItemEqualToValue={(a, b) => a.id === b.id} disabled={!lecturer} > {selectedCourses.map((c) => ( {c.code} ))} Mata kuliah tidak ditemukan. {(group: CourseGroup) => ( {group.value} {(c: Course) => ( {c.code} - {c.name} )} )}
)}
); } function EditForm({ open, onOpenChange, editing, courses, lecturers, academicTerms, courseClassAssignments, }: { open: boolean; onOpenChange: (open: boolean) => void; editing: CourseClass | null; courses: Course[]; lecturers: Lecturer[]; academicTerms: AcademicTerm[]; courseClassAssignments: CourseClassAssignment[]; }) { const [academicTermId, setAcademicTermId] = useState( editing ? String(editing.academic_term_id) : '', ); const [course, setCourse] = useState( editing ? (courses.find((c) => c.id === editing.course_id) ?? null) : null, ); const [lecturer, setLecturer] = useState( editing ? (lecturers.find((l) => l.id === editing.lecturer_id) ?? null) : null, ); const selectedTerm = academicTerms.find( (term) => String(term.id) === academicTermId, ); const takenCourseIds = new Set( courseClassAssignments .filter( (a) => String(a.academic_term_id) === academicTermId && a.id !== editing?.id, ) .map((a) => a.course_id), ); const lecturerDepartmentIds = new Set( lecturer ? lecturer.departments.map((d) => d.id) : [], ); const availableCourses = lecturer ? courses.filter( (c) => lecturerDepartmentIds.has(c.department_id) && !takenCourseIds.has(c.id) && courseMatchesTermParity(c, selectedTerm), ) : []; const courseGroups = groupCoursesByDepartmentAndSemester(availableCourses); return ( onOpenChange(false)} > {({ errors }) => editing && (
{ setLecturer(value); setCourse(null); }} itemToStringLabel={(lect) => `${lect.user?.profile?.full_name ?? 'N/A'} - ${lect.lecturer_number}` } isItemEqualToValue={(a, b) => a.id === b.id} > Dosen tidak ditemukan. {(lect: Lecturer) => ( {lect.user?.profile ?.full_name ?? 'N/A'}{' '} - {lect.lecturer_number} )}
`${c.code} - ${c.name}` } isItemEqualToValue={(a, b) => a.id === b.id} disabled={!lecturer} > Mata kuliah tidak ditemukan. {(group: CourseGroup) => ( {group.value} {(c: Course) => ( {c.code} -{' '} {c.name} )} )}
) }
); } function DuplicateForm({ open, onOpenChange, academicTerms, }: { open: boolean; onOpenChange: (open: boolean) => void; academicTerms: AcademicTerm[]; }) { const [sourceTermId, setSourceTermId] = useState(''); const [targetTermId, setTargetTermId] = useState(''); const sourceTerm = academicTerms.find( (term) => String(term.id) === sourceTermId, ); const targetOptions = academicTerms.filter( (term) => String(term.id) !== sourceTermId && (!sourceTerm || term.semester === sourceTerm.semester), ); function reset() { setSourceTermId(''); setTargetTermId(''); } return ( { onOpenChange(false); reset(); }} > {({ errors }) => (

Menyalin semua kelas mata kuliah (mata kuliah, dosen pengampu, metode) dari periode sumber ke periode tujuan. Periode tujuan hanya bisa dipilih dari semester yang sama (Ganjil ke Ganjil, Genap ke Genap). Mata kuliah yang sudah punya kelas di periode tujuan akan dilewati.

)}
); }