import { Head, router } from '@inertiajs/react'; import { Clock, MapPin, Pencil, Plus, Trash2, User, Video, } from 'lucide-react'; import { useState } from 'react'; 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 { RowActions } from '@/components/row-actions'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Combobox, ComboboxCollection, ComboboxContent, ComboboxEmpty, ComboboxGroup, ComboboxInput, ComboboxItem, ComboboxLabel, ComboboxList, } from '@/components/ui/combobox'; import { Input } from '@/components/ui/input'; 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 { cn } from '@/lib/utils'; import { destroy, index as scheduleIndex, store, update, } from '@/routes/admin/academic-classes/schedules'; import { formatAcademicTermLabel } from '@/types/academic-term'; import { ClassMethodLabels } from '@/types/course-class'; import type { ClassMethod } from '@/types/course-class'; import type { Schedule } from '@/types/schedule'; import { DayOfWeekLabels, DaysOfWeek } from '@/types/schedule'; type CourseClassOption = { id: number; course: { id: number; code: string; name: string; semester_number: number | null; department: { id: number; name: string } | null; } | null; lecturer: { id: number; user: { profile: { full_name: string } | null } | null; } | null; academic_term: { id: number; academic_year: string; semester: string; } | null; }; type CourseClassGroup = { value: string; items: CourseClassOption[] }; type AcademicTermOption = { id: number; academic_year: string; semester: string; }; type DepartmentOption = { id: number; name: string; }; type Props = { schedules: Schedule[]; courseClasses: CourseClassOption[]; academicTerms: AcademicTermOption[]; departments: DepartmentOption[]; semesterNumbers: number[]; isPersonalView: boolean; highlight?: number; filters: { academic_term_id?: string; department_id?: string; semester_number?: string; }; }; const UNSCHEDULED = '__unscheduled__'; const BOARD_COLUMNS = [...DaysOfWeek, UNSCHEDULED] as const; const DEPARTMENT_PALETTE = [ { border: 'border-l-blue-500 bg-blue-50/60 dark:bg-blue-950/20', dot: 'bg-blue-500' }, { border: 'border-l-emerald-500 bg-emerald-50/60 dark:bg-emerald-950/20', dot: 'bg-emerald-500' }, { border: 'border-l-amber-500 bg-amber-50/60 dark:bg-amber-950/20', dot: 'bg-amber-500' }, { border: 'border-l-violet-500 bg-violet-50/60 dark:bg-violet-950/20', dot: 'bg-violet-500' }, { border: 'border-l-rose-500 bg-rose-50/60 dark:bg-rose-950/20', dot: 'bg-rose-500' }, { border: 'border-l-cyan-500 bg-cyan-50/60 dark:bg-cyan-950/20', dot: 'bg-cyan-500' }, { border: 'border-l-orange-500 bg-orange-50/60 dark:bg-orange-950/20', dot: 'bg-orange-500' }, { border: 'border-l-fuchsia-500 bg-fuchsia-50/60 dark:bg-fuchsia-950/20', dot: 'bg-fuchsia-500' }, ] as const; function departmentPalette(departmentId: number | undefined) { if (!departmentId) { return { border: 'border-l-border', dot: 'bg-muted-foreground/40' }; } return DEPARTMENT_PALETTE[departmentId % DEPARTMENT_PALETTE.length]; } function courseClassLabel(courseClass: { course: { code: string; name: string } | null; } | null): string { return `${courseClass?.course?.code ?? ''} ${courseClass?.course?.name ?? ''}`; } function courseClassOptionLabel(option: CourseClassOption): string { return `${option.course?.code ?? ''} - ${option.course?.name ?? ''}`; } function groupCourseClassesByDepartment( options: CourseClassOption[], ): CourseClassGroup[] { const groups: CourseClassGroup[] = []; let currentKey: string | null = null; for (const option of options) { const key = `${option.course?.department?.name ?? 'Tanpa Jurusan'} — Semester ${option.course?.semester_number ?? 'Tidak ditentukan'}`; if (key !== currentKey) { currentKey = key; groups.push({ value: key, items: [] }); } groups[groups.length - 1].items.push(option); } return groups; } function toTimeInput(value: string | null): string { return value ? value.slice(0, 5) : ''; } export default function ScheduleIndex({ schedules, courseClasses, academicTerms, departments, semesterNumbers, isPersonalView, highlight, filters, }: Props) { const [createOpen, setCreateOpen] = useState(false); const [editing, setEditing] = useState(null); const [deleting, setDeleting] = useState(null); const { hasPermission } = usePermissions(); const canCreate = hasPermission('create-schedules'); const canUpdate = hasPermission('update-schedules'); const canDelete = hasPermission('delete-schedules'); const filterFields: FilterField[] = [ { key: 'academic_term_id', label: 'Periode Akademik', options: academicTerms.map((term) => ({ value: String(term.id), label: formatAcademicTermLabel(term), })), }, ...(isPersonalView ? [] : [ { key: 'department_id', label: 'Jurusan', options: departments.map((department) => ({ value: String(department.id), label: department.name, })), }, { key: 'semester_number', label: 'Semester', options: semesterNumbers.map((semester) => ({ value: String(semester), label: String(semester), })), }, ]), ]; const { applyFilters } = useServerTable({ route: () => scheduleIndex.url(), pagination: { current_page: 1, last_page: 1, per_page: 999999, total: schedules.length, }, filters, }); function handleApplyFilters(newFilters: Record) { // Without an explicit `academic_term_id`, the backend defaults it back // to the active term, so clearing it needs to be sent explicitly // instead of just omitting the key. const clearedAcademicTerm = Boolean(filters.academic_term_id) && !newFilters.academic_term_id; applyFilters({ ...newFilters, ...(clearedAcademicTerm ? { academic_term_id: '' } : {}), }); } function handleDelete() { if (!deleting) { return; } router.delete(destroy(deleting.id), { onSuccess: () => setDeleting(null), }); } const grouped = new Map(); for (const schedule of schedules) { const key = schedule.day_of_week ?? UNSCHEDULED; grouped.set(key, [...(grouped.get(key) ?? []), schedule]); } const columns = BOARD_COLUMNS.filter( (day) => day !== UNSCHEDULED || (grouped.get(UNSCHEDULED)?.length ?? 0) > 0, ); const departmentsInView = Array.from( new Map( schedules .map((schedule) => schedule.course_class?.course?.department) .filter((department): department is { id: number; name: string } => !!department) .map((department) => [department.id, department]), ).values(), ); return ( <>
{canCreate && ( )}
} />

Geser ke samping untuk melihat hari lainnya.

{departmentsInView.length > 1 && (
{departmentsInView.map((department) => ( {department.name} ))}
)}
{ if (!open) { setEditing(null); } }} editing={editing} courseClasses={courseClasses} /> {schedules.length === 0 ? (

Belum ada data jadwal.

) : (
{columns.map((day) => { const items = grouped.get(day) ?? []; return (
{day === UNSCHEDULED ? 'Belum Diatur' : DayOfWeekLabels[day]} {items.length}
{items.length === 0 ? (

Tidak ada jadwal

) : ( items.map((schedule) => ( {courseClassLabel( schedule.course_class ?? { course: null, }, )} ), show: canUpdate, onClick: () => setEditing( schedule, ), }, { label: 'Hapus', icon: ( ), show: canDelete, onClick: () => setDeleting( schedule, ), }, ]} /> {(schedule.start_time || schedule.end_time) && ( {toTimeInput( schedule.start_time, ) || '-'}{' '} -{' '} {toTimeInput( schedule.end_time, ) || '-'} )} {schedule.course_class ?.lecturer && ( {schedule .course_class .lecturer .user ?.profile ?.full_name ?? 'N/A'} )} {schedule.room && ( {schedule.room} )} {schedule.online_link && ( )} {schedule.course_class ?.method && ( {ClassMethodLabels[ schedule .course_class .method as ClassMethod ] ?? schedule .course_class .method} )} )) )}
); })}
)} { if (!open) { setDeleting(null); } }} title="Hapus Jadwal" description={(schedule) => `Apakah Anda yakin ingin menghapus jadwal "${schedule.course_class?.course?.name ?? 'ini'}"? Tindakan ini tidak dapat dibatalkan.` } onConfirm={handleDelete} /> ); } function CourseClassField({ courseClasses, value, onChange, disabled, }: { courseClasses: CourseClassOption[]; value: CourseClassOption | null; onChange: (value: CourseClassOption | null) => void; disabled?: boolean; }) { const groups = groupCourseClassesByDepartment(courseClasses); return ( a.id === b.id} disabled={disabled} > Kelas tidak ditemukan. {(group: CourseClassGroup) => ( {group.value} {(option: CourseClassOption) => (
{courseClassOptionLabel(option)} {option.lecturer?.user?.profile ?.full_name ?? 'N/A'} {option.academic_term && ` • ${formatAcademicTermLabel(option.academic_term)}`}
)}
)}
); } function CreateForm({ open, onOpenChange, courseClasses, }: { open: boolean; onOpenChange: (open: boolean) => void; courseClasses: CourseClassOption[]; }) { const [courseClass, setCourseClass] = useState( null, ); return ( { onOpenChange(false); setCourseClass(null); }} > {({ errors }) => (
)}
); } function EditForm({ open, onOpenChange, editing, courseClasses, }: { open: boolean; onOpenChange: (open: boolean) => void; editing: Schedule | null; courseClasses: CourseClassOption[]; }) { const [courseClass, setCourseClass] = useState( editing ? (courseClasses.find((c) => c.id === editing.course_class_id) ?? null) : null, ); return ( onOpenChange(false)} > {({ errors }) => editing && (
) }
); }