import { Head, InfiniteScroll, router } from '@inertiajs/react'; import { format } from 'date-fns'; import { Clock, ClipboardList, Paperclip, Pencil, Plus, Trash2, Upload, } from 'lucide-react'; import { useState } from 'react'; import { AttachmentPreviewDialog } from '@/components/attachment-preview-dialog'; import { DateTimeField } from '@/components/datetime-field'; import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog'; import { FileUploadField } from '@/components/file-upload-field'; import type { FilterField, FilterOptionGroup, } 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 { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from '@/components/ui/accordion'; 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 { Textarea } from '@/components/ui/textarea'; import { usePermissions } from '@/hooks/use-permissions'; import { useServerTable } from '@/hooks/use-server-table'; import { index as assignmentIndex, destroy, store, submit, update, } from '@/routes/admin/academic-classes/assignments'; import { index as submissionsIndex } from '@/routes/admin/academic-classes/assignments/submissions'; import { formatAcademicTermLabel } from '@/types/academic-term'; import type { Assignment } from '@/types/assignment'; import { AssignmentStatusLabels, AssignmentStatuses } from '@/types/assignment'; type CourseClassOption = { id: number; course: { id: number; code: string; name: string; semester_number: number | null; department: { id: number; name: string } | null; } | null; }; type CourseClassGroup = { value: string; items: CourseClassOption[] }; type AcademicTermOption = { id: number; academic_year: string; semester: string; }; type Props = { assignments: { data: Assignment[]; current_page: number; last_page: number; per_page: number; total: number; }; courseClasses: CourseClassOption[]; academicTerms: AcademicTermOption[]; highlight?: number; filters: { course_class_id?: string; academic_term_id?: string; }; }; function courseClassLabel(courseClass: CourseClassOption): string { return `${courseClass.course?.code ?? ''} - ${courseClass.course?.name ?? ''}`; } function percentageOf(part: number, total: number): number { return total > 0 ? Math.round((part / total) * 100) : 0; } /** Turns red as the deadline passes, amber once it's within 3 days. */ function deadlineTextClass(deadline: string): string { const hoursLeft = (new Date(deadline).getTime() - Date.now()) / 3_600_000; if (hoursLeft <= 0) { return 'font-medium text-destructive'; } if (hoursLeft <= 24) { return 'text-destructive'; } if (hoursLeft <= 72) { return 'text-amber-600 dark:text-amber-500'; } return 'text-muted-foreground'; } 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 courseClassFilterGroups( courseClasses: CourseClassOption[], ): FilterOptionGroup[] { return groupCourseClassesByDepartment(courseClasses).map((group) => ({ label: group.value, options: group.items.map((option) => ({ value: String(option.id), label: courseClassLabel(option), })), })); } function CourseClassField({ courseClasses, value, onChange, }: { courseClasses: CourseClassOption[]; value: CourseClassOption | null; onChange: (value: CourseClassOption | null) => void; }) { const groups = groupCourseClassesByDepartment(courseClasses); return ( a.id === b.id} > Kelas tidak ditemukan. {(group: CourseClassGroup) => ( {group.value} {(option: CourseClassOption) => ( {courseClassLabel(option)} )} )} ); } export default function AssignmentIndex({ assignments, courseClasses, academicTerms, highlight, filters, }: Props) { const [createOpen, setCreateOpen] = useState(false); const [editing, setEditing] = useState(null); const [deleting, setDeleting] = useState(null); const [submitting, setSubmitting] = useState(null); const { hasPermission } = usePermissions(); const canCreate = hasPermission('create-assignments'); const canUpdate = hasPermission('update-assignments'); const canDelete = hasPermission('delete-assignments'); const canViewSubmissions = hasPermission('view-assignment-submissions'); const canSubmit = hasPermission('submit-assignments'); const filterFields: FilterField[] = [ { key: 'academic_term_id', label: 'Periode Akademik', options: academicTerms.map((term) => ({ value: String(term.id), label: formatAcademicTermLabel(term), })), }, { key: 'course_class_id', label: 'Kelas', type: 'combobox' as const, groups: courseClassFilterGroups(courseClasses), }, ]; const pagination = { current_page: assignments.current_page, last_page: assignments.last_page, per_page: assignments.per_page, total: assignments.total, }; const { search, handleSearchChange, applyFilters } = useServerTable({ route: () => assignmentIndex.url(), pagination, filters, resetKeys: ['assignments'], }); function handleApplyFilters(newFilters: Record) { // Tanpa `academic_term_id` eksplisit, backend akan kembali ke // periode aktif, jadi menghapus filter ini perlu dikirim eksplisit // alih-alih hanya menghilangkan key-nya. 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), }); } return ( <> Menampilkan tugas dari notifikasi. { router.get( assignmentIndex.url(), {}, { replace: true, preserveState: true, }, ); }} className="ml-1 text-primary underline underline-offset-4 hover:text-primary/80" > Tampilkan semua ) } actions={ canCreate && ( setCreateOpen(true)} > Tambah ) } /> { if (!open) { setEditing(null); } }} editing={editing} courseClasses={courseClasses} /> { if (!open) { setSubmitting(null); } }} assignment={submitting} /> handleSearchChange(event.target.value) } className="max-w-sm" /> {assignments.data.length === 0 ? ( Belum ada tugas. ) : ( ( Memuat tugas... )} > {assignments.data.map((assignment) => { const mySubmission = assignment.submissions?.[0]; return ( {assignment.title} {assignment.course_class && ( {assignment.course_class .course?.code ?? ''}{' '} {assignment.course_class .course?.name ?? ''} )} ), show: canSubmit && assignment.status === 'open', onClick: () => setSubmitting( assignment, ), }, { label: 'Pengumpulan', icon: ( ), show: canViewSubmissions, href: submissionsIndex.url( assignment.id, ), }, { label: 'Edit', icon: ( ), show: canUpdate, onClick: () => setEditing(assignment), }, { label: 'Hapus', icon: ( ), show: canDelete, onClick: () => setDeleting(assignment), }, ]} /> { AssignmentStatusLabels[ assignment.status ] } {assignment.course_class ?.academic_term && ( {formatAcademicTermLabel( assignment.course_class .academic_term, )} )} {format( new Date(assignment.deadline), 'd MMM yyyy, HH:mm', )} {assignment.attachment_url && assignment.attachment_name ? ( ) : null} {canSubmit ? ( {mySubmission?.status === 'submitted' ? 'Sudah Mengumpulkan' : 'Belum Mengumpulkan'} ) : ( <> {assignment.submissions_count.toLocaleString( 'id-ID', )}{' '} /{' '} {( assignment .course_class ?.enrollments_count ?? 0 ).toLocaleString( 'id-ID', )}{' '} Pengumpulan ( {percentageOf( assignment.submissions_count, assignment .course_class ?.enrollments_count ?? 0, )} %) {assignment.graded_submissions_count.toLocaleString( 'id-ID', )}{' '} /{' '} {assignment.submissions_count.toLocaleString( 'id-ID', )}{' '} Dinilai ( {percentageOf( assignment.graded_submissions_count, assignment.submissions_count, )} %) > )} {assignment.description && ( Deskripsi { assignment.description } )} ); })} )} { if (!open) { setDeleting(null); } }} title="Hapus Tugas" description={(assignment) => `Apakah Anda yakin ingin menghapus tugas "${assignment.title}"? Tindakan ini tidak dapat dibatalkan.` } onConfirm={handleDelete} /> > ); } function CreateForm({ open, onOpenChange, courseClasses, }: { open: boolean; onOpenChange: (open: boolean) => void; courseClasses: CourseClassOption[]; }) { const [courseClass, setCourseClass] = useState( null, ); return ( { onOpenChange(false); setCourseClass(null); }} > {({ errors }) => ( Kelas * Judul * Deskripsi )} ); } function EditForm({ open, onOpenChange, editing, courseClasses, }: { open: boolean; onOpenChange: (open: boolean) => void; editing: Assignment | null; courseClasses: CourseClassOption[]; }) { const [courseClass, setCourseClass] = useState( editing ? (courseClasses.find((c) => c.id === editing.course_class_id) ?? null) : null, ); return ( onOpenChange(false)} > {({ errors }) => editing && ( Kelas{' '} * Judul{' '} * Deskripsi Status{' '} * {AssignmentStatuses.map((status) => ( {AssignmentStatusLabels[status]} ))} Menutup tugas akan mencegah mahasiswa mengumpulkan, terlepas dari batas waktu. ) } ); } function SubmitForm({ open, onOpenChange, assignment, }: { open: boolean; onOpenChange: (open: boolean) => void; assignment: Assignment | null; }) { const mySubmission = assignment?.submissions?.[0]; return ( onOpenChange(false)} > {({ errors }) => ( Catatan )} ); }
Belum ada tugas.
Memuat tugas...
{ assignment.description }
Menutup tugas akan mencegah mahasiswa mengumpulkan, terlepas dari batas waktu.