From b0e16a6cd5004b2e12207afe71b8ffacddb7e831 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Mon, 24 Aug 2026 18:15:44 +0700 Subject: [PATCH] feat: add assignment and submission management features - Created SubmissionService to handle submission logic for assignments. - Added migrations for assignments and submissions tables. - Implemented AssignmentSeeder and SubmissionSeeder for initial data. - Updated DatabaseSeeder to include new seeders. - Enhanced app sidebar to include assignments navigation. - Developed datetime field component for better date and time input. - Created assignment management pages with data tables for assignments and submissions. - Implemented forms for creating and editing assignments and submissions. - Added routes for assignment and submission management in admin panel. - Defined types for assignments and submissions to improve type safety. --- app/Enums/SubmissionStatus.php | 17 + .../Admin/Manage/AssignmentController.php | 54 +++ .../Admin/Manage/SubmissionController.php | 51 +++ .../Admin/Manage/AssignmentRequest.php | 25 + .../Admin/Manage/SubmissionRequest.php | 41 ++ app/Models/Assignment.php | 57 +++ app/Models/CourseClass.php | 5 + app/Models/Student.php | 5 + app/Models/Submission.php | 58 +++ .../Admin/Manage/AssignmentService.php | 57 +++ .../Admin/Manage/SubmissionService.php | 68 +++ ..._08_24_000001_create_assignments_table.php | 26 ++ ..._08_24_000002_create_submissions_table.php | 31 ++ database/seeders/AssignmentSeeder.php | 31 ++ database/seeders/DatabaseSeeder.php | 2 + database/seeders/SubmissionSeeder.php | 37 ++ resources/js/components/app-sidebar.tsx | 32 +- resources/js/components/date-picker.tsx | 1 - resources/js/components/datetime-field.tsx | 67 +++ resources/js/components/ui/calendar.tsx | 2 - .../admin/manage/assignments/columns.tsx | 130 ++++++ .../pages/admin/manage/assignments/index.tsx | 363 +++++++++++++++ .../admin/manage/assignments/submissions.tsx | 428 ++++++++++++++++++ resources/js/types/assignment.ts | 17 + resources/js/types/submission.ts | 31 ++ routes/admin.php | 11 + 26 files changed, 1632 insertions(+), 15 deletions(-) create mode 100644 app/Enums/SubmissionStatus.php create mode 100644 app/Http/Controllers/Admin/Manage/AssignmentController.php create mode 100644 app/Http/Controllers/Admin/Manage/SubmissionController.php create mode 100644 app/Http/Requests/Admin/Manage/AssignmentRequest.php create mode 100644 app/Http/Requests/Admin/Manage/SubmissionRequest.php create mode 100644 app/Models/Assignment.php create mode 100644 app/Models/Submission.php create mode 100644 app/Services/Admin/Manage/AssignmentService.php create mode 100644 app/Services/Admin/Manage/SubmissionService.php create mode 100644 database/migrations/2026_08_24_000001_create_assignments_table.php create mode 100644 database/migrations/2026_08_24_000002_create_submissions_table.php create mode 100644 database/seeders/AssignmentSeeder.php create mode 100644 database/seeders/SubmissionSeeder.php create mode 100644 resources/js/components/datetime-field.tsx create mode 100644 resources/js/pages/admin/manage/assignments/columns.tsx create mode 100644 resources/js/pages/admin/manage/assignments/index.tsx create mode 100644 resources/js/pages/admin/manage/assignments/submissions.tsx create mode 100644 resources/js/types/assignment.ts create mode 100644 resources/js/types/submission.ts diff --git a/app/Enums/SubmissionStatus.php b/app/Enums/SubmissionStatus.php new file mode 100644 index 0000000..9ca1eb7 --- /dev/null +++ b/app/Enums/SubmissionStatus.php @@ -0,0 +1,17 @@ + 'Belum Mengumpulkan', + self::Submitted => 'Sudah Mengumpulkan', + }; + } +} diff --git a/app/Http/Controllers/Admin/Manage/AssignmentController.php b/app/Http/Controllers/Admin/Manage/AssignmentController.php new file mode 100644 index 0000000..1bf5846 --- /dev/null +++ b/app/Http/Controllers/Admin/Manage/AssignmentController.php @@ -0,0 +1,54 @@ + $this->service->paginated(...$request->validatedWithDefaults()), + 'courseClasses' => $this->courseClassService->getAllForSelect(), + ]); + } + + public function store(AssignmentRequest $request): RedirectResponse + { + $this->service->create($request->validated(), $request->file('attachment')); + + Inertia::flash('toast', ['type' => 'success', 'message' => 'Tugas berhasil ditambahkan.']); + + return to_route('admin.manage.assignments.index'); + } + + public function update(AssignmentRequest $request, Assignment $assignment): RedirectResponse + { + $this->service->update($assignment, $request->validated(), $request->file('attachment')); + + Inertia::flash('toast', ['type' => 'success', 'message' => 'Tugas berhasil diperbarui.']); + + return to_route('admin.manage.assignments.index'); + } + + public function destroy(Assignment $assignment): RedirectResponse + { + $this->service->delete($assignment); + + return Inertia::flash('toast', ['type' => 'success', 'message' => 'Tugas berhasil dihapus.'])->back(); + } +} diff --git a/app/Http/Controllers/Admin/Manage/SubmissionController.php b/app/Http/Controllers/Admin/Manage/SubmissionController.php new file mode 100644 index 0000000..7753042 --- /dev/null +++ b/app/Http/Controllers/Admin/Manage/SubmissionController.php @@ -0,0 +1,51 @@ +load(['courseClass.course', 'courseClass.lecturer.user.profile', 'courseClass.academicTerm']); + + return Inertia::render('admin/manage/assignments/submissions', [ + 'assignment' => $assignment, + 'submissions' => $this->service->forAssignment($assignment), + 'availableStudents' => $this->service->availableStudents($assignment), + ]); + } + + public function store(SubmissionRequest $request, Assignment $assignment): RedirectResponse + { + $this->service->create($assignment, $request->validated(), $request->file('file')); + + return Inertia::flash('toast', ['type' => 'success', 'message' => 'Pengumpulan berhasil ditambahkan.'])->back(); + } + + public function update(SubmissionRequest $request, Assignment $assignment, Submission $submission): RedirectResponse + { + $this->service->update($submission, $request->validated(), $request->file('file')); + + return Inertia::flash('toast', ['type' => 'success', 'message' => 'Pengumpulan berhasil diperbarui.'])->back(); + } + + public function destroy(Assignment $assignment, Submission $submission): RedirectResponse + { + $this->service->delete($submission); + + return Inertia::flash('toast', ['type' => 'success', 'message' => 'Pengumpulan berhasil dihapus.'])->back(); + } +} diff --git a/app/Http/Requests/Admin/Manage/AssignmentRequest.php b/app/Http/Requests/Admin/Manage/AssignmentRequest.php new file mode 100644 index 0000000..30a8f38 --- /dev/null +++ b/app/Http/Requests/Admin/Manage/AssignmentRequest.php @@ -0,0 +1,25 @@ + ['required', 'integer', Rule::exists('course_classes', 'id')], + 'title' => ['required', 'string', 'max:150'], + 'description' => ['nullable', 'string'], + 'deadline' => ['required', 'date'], + 'attachment' => ['nullable', 'file', 'max:10240', 'mimes:pdf,doc,docx,ppt,pptx,xls,xlsx,jpg,jpeg,png,mp4,zip'], + ]; + } +} diff --git a/app/Http/Requests/Admin/Manage/SubmissionRequest.php b/app/Http/Requests/Admin/Manage/SubmissionRequest.php new file mode 100644 index 0000000..6a4499f --- /dev/null +++ b/app/Http/Requests/Admin/Manage/SubmissionRequest.php @@ -0,0 +1,41 @@ +route('assignment'); + $submission = $this->route('submission'); + + $rules = [ + 'notes' => ['nullable', 'string'], + 'status' => ['required', Rule::enum(SubmissionStatus::class)], + 'submitted_at' => ['nullable', 'date'], + 'score' => ['nullable', 'numeric', 'min:0', 'max:100'], + 'lecturer_feedback' => ['nullable', 'string'], + 'file' => ['nullable', 'file', 'max:10240', 'mimes:pdf,doc,docx,ppt,pptx,xls,xlsx,jpg,jpeg,png,mp4,zip'], + ]; + + if (! $submission) { + $rules['student_id'] = [ + 'required', + 'integer', + Rule::exists('class_enrollments', 'student_id')->where('course_class_id', $assignment->course_class_id), + Rule::unique('submissions', 'student_id')->where('assignment_id', $assignment->id), + ]; + } + + return $rules; + } +} diff --git a/app/Models/Assignment.php b/app/Models/Assignment.php new file mode 100644 index 0000000..0aa2466 --- /dev/null +++ b/app/Models/Assignment.php @@ -0,0 +1,57 @@ + 'datetime', + ]; + } + + public function registerMediaCollections(): void + { + $this->addMediaCollection('assignment_attachment')->singleFile(); + } + + public function courseClass(): BelongsTo + { + return $this->belongsTo(CourseClass::class); + } + + public function submissions(): HasMany + { + return $this->hasMany(Submission::class); + } + + protected function attachmentUrl(): Attribute + { + return Attribute::make( + get: fn () => $this->getFirstMediaUrl('assignment_attachment') ?: null, + ); + } + + protected function attachmentName(): Attribute + { + return Attribute::make( + get: fn () => $this->getFirstMedia('assignment_attachment')?->file_name, + ); + } +} diff --git a/app/Models/CourseClass.php b/app/Models/CourseClass.php index 08fcef5..ed23153 100644 --- a/app/Models/CourseClass.php +++ b/app/Models/CourseClass.php @@ -46,4 +46,9 @@ public function materials(): HasMany { return $this->hasMany(Material::class); } + + public function assignments(): HasMany + { + return $this->hasMany(Assignment::class); + } } diff --git a/app/Models/Student.php b/app/Models/Student.php index 5f252bf..2b5097d 100644 --- a/app/Models/Student.php +++ b/app/Models/Student.php @@ -41,4 +41,9 @@ public function enrollments(): HasMany { return $this->hasMany(ClassEnrollment::class); } + + public function submissions(): HasMany + { + return $this->hasMany(Submission::class); + } } diff --git a/app/Models/Submission.php b/app/Models/Submission.php new file mode 100644 index 0000000..42af2ad --- /dev/null +++ b/app/Models/Submission.php @@ -0,0 +1,58 @@ + SubmissionStatus::class, + 'submitted_at' => 'datetime', + 'score' => 'decimal:2', + ]; + } + + public function registerMediaCollections(): void + { + $this->addMediaCollection('submission_file')->singleFile(); + } + + public function assignment(): BelongsTo + { + return $this->belongsTo(Assignment::class); + } + + public function student(): BelongsTo + { + return $this->belongsTo(Student::class); + } + + protected function fileUrl(): Attribute + { + return Attribute::make( + get: fn () => $this->getFirstMediaUrl('submission_file') ?: null, + ); + } + + protected function fileName(): Attribute + { + return Attribute::make( + get: fn () => $this->getFirstMedia('submission_file')?->file_name, + ); + } +} diff --git a/app/Services/Admin/Manage/AssignmentService.php b/app/Services/Admin/Manage/AssignmentService.php new file mode 100644 index 0000000..d43808a --- /dev/null +++ b/app/Services/Admin/Manage/AssignmentService.php @@ -0,0 +1,57 @@ +select(['id', 'course_class_id', 'title', 'description', 'deadline']) + ->withCount('submissions') + ->with('courseClass.course:id,code,name') + ->when($search, fn ($q) => $q->where('title', 'like', "%{$search}%")) + ->orderBy($sort, $direction) + ->paginate($perPage); + } + + public function create(array $data, ?UploadedFile $file): Assignment + { + $assignment = Assignment::create([ + 'course_class_id' => $data['course_class_id'], + 'title' => $data['title'], + 'description' => $data['description'] ?? null, + 'deadline' => $data['deadline'], + ]); + + if ($file) { + $assignment->addMedia($file)->toMediaCollection('assignment_attachment'); + } + + return $assignment; + } + + public function update(Assignment $assignment, array $data, ?UploadedFile $file): Assignment + { + $assignment->course_class_id = $data['course_class_id']; + $assignment->title = $data['title']; + $assignment->description = $data['description'] ?? null; + $assignment->deadline = $data['deadline']; + $assignment->update(); + + if ($file) { + $assignment->addMedia($file)->toMediaCollection('assignment_attachment'); + } + + return $assignment; + } + + public function delete(Assignment $assignment): bool + { + return $assignment->delete(); + } +} diff --git a/app/Services/Admin/Manage/SubmissionService.php b/app/Services/Admin/Manage/SubmissionService.php new file mode 100644 index 0000000..f4964d7 --- /dev/null +++ b/app/Services/Admin/Manage/SubmissionService.php @@ -0,0 +1,68 @@ +submissions() + ->with(['student.user.profile', 'student.department']) + ->latest('created_at') + ->get(); + } + + public function availableStudents(Assignment $assignment): Collection + { + return Student::query() + ->whereHas('enrollments', fn ($q) => $q->where('course_class_id', $assignment->course_class_id)) + ->whereDoesntHave('submissions', fn ($q) => $q->where('assignment_id', $assignment->id)) + ->with(['user.profile', 'department']) + ->get(); + } + + public function create(Assignment $assignment, array $data, ?UploadedFile $file): Submission + { + $submission = $assignment->submissions()->create([ + 'student_id' => $data['student_id'], + 'notes' => $data['notes'] ?? null, + 'status' => $data['status'], + 'submitted_at' => $data['submitted_at'] ?? null, + 'score' => $data['score'] ?? null, + 'lecturer_feedback' => $data['lecturer_feedback'] ?? null, + ]); + + if ($file) { + $submission->addMedia($file)->toMediaCollection('submission_file'); + } + + return $submission; + } + + public function update(Submission $submission, array $data, ?UploadedFile $file): Submission + { + $submission->notes = $data['notes'] ?? null; + $submission->status = $data['status']; + $submission->submitted_at = $data['submitted_at'] ?? null; + $submission->score = $data['score'] ?? null; + $submission->lecturer_feedback = $data['lecturer_feedback'] ?? null; + $submission->update(); + + if ($file) { + $submission->addMedia($file)->toMediaCollection('submission_file'); + } + + return $submission; + } + + public function delete(Submission $submission): bool + { + return $submission->delete(); + } +} diff --git a/database/migrations/2026_08_24_000001_create_assignments_table.php b/database/migrations/2026_08_24_000001_create_assignments_table.php new file mode 100644 index 0000000..bd03011 --- /dev/null +++ b/database/migrations/2026_08_24_000001_create_assignments_table.php @@ -0,0 +1,26 @@ +id(); + $table->foreignId('course_class_id')->constrained()->cascadeOnDelete(); + $table->string('title', 150); + $table->text('description')->nullable(); + $table->timestamp('deadline'); + $table->timestamps(); + $table->softDeletes(); + }); + } + + public function down(): void + { + Schema::dropIfExists('assignments'); + } +}; diff --git a/database/migrations/2026_08_24_000002_create_submissions_table.php b/database/migrations/2026_08_24_000002_create_submissions_table.php new file mode 100644 index 0000000..f6ea930 --- /dev/null +++ b/database/migrations/2026_08_24_000002_create_submissions_table.php @@ -0,0 +1,31 @@ +id(); + $table->foreignId('assignment_id')->constrained()->cascadeOnDelete(); + $table->foreignId('student_id')->constrained()->cascadeOnDelete(); + $table->text('notes')->nullable(); + $table->enum('status', array_values(SubmissionStatus::cases()))->nullable()->default(SubmissionStatus::NotSubmitted->value); + $table->timestamp('submitted_at')->nullable(); + $table->decimal('score', 5, 2)->nullable(); + $table->text('lecturer_feedback')->nullable(); + $table->timestamps(); + + $table->unique(['assignment_id', 'student_id']); + }); + } + + public function down(): void + { + Schema::dropIfExists('submissions'); + } +}; diff --git a/database/seeders/AssignmentSeeder.php b/database/seeders/AssignmentSeeder.php new file mode 100644 index 0000000..515ad7f --- /dev/null +++ b/database/seeders/AssignmentSeeder.php @@ -0,0 +1,31 @@ + 1, + 'title' => 'Tugas Membuat Landing Page', + 'description' => 'Buat landing page sederhana dengan HTML & CSS', + 'deadline' => '2026-08-20 23:59:00', + 'created_at' => '2026-08-05 09:00:00', + 'updated_at' => '2026-08-05 09:00:00', + ], + [ + 'course_class_id' => 2, + 'title' => 'Studi Kasus Kualitas Produk', + 'description' => 'Analisis studi kasus pengendalian kualitas produk', + 'deadline' => '2026-08-25 23:59:00', + 'created_at' => '2026-08-05 10:00:00', + 'updated_at' => '2026-08-05 10:00:00', + ], + ]); + } +} diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 1b1f13d..b7cb346 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -21,6 +21,8 @@ public function run(): void CourseClassSeeder::class, ClassEnrollmentSeeder::class, MaterialSeeder::class, + AssignmentSeeder::class, + SubmissionSeeder::class, ]); } } diff --git a/database/seeders/SubmissionSeeder.php b/database/seeders/SubmissionSeeder.php new file mode 100644 index 0000000..1334985 --- /dev/null +++ b/database/seeders/SubmissionSeeder.php @@ -0,0 +1,37 @@ + 1, + 'student_id' => 1, + 'notes' => 'Sudah selesai, mohon direview', + 'status' => 'submitted', + 'submitted_at' => '2026-08-18 20:00:00', + 'score' => 85, + 'lecturer_feedback' => 'Bagus, tampilan rapi', + 'created_at' => '2026-08-18 20:00:00', + 'updated_at' => '2026-08-19 07:00:00', + ], + [ + 'assignment_id' => 2, + 'student_id' => 2, + 'notes' => null, + 'status' => 'not_submitted', + 'submitted_at' => null, + 'score' => null, + 'lecturer_feedback' => null, + 'created_at' => '2026-08-05 10:05:00', + 'updated_at' => '2026-08-05 10:05:00', + ], + ]); + } +} diff --git a/resources/js/components/app-sidebar.tsx b/resources/js/components/app-sidebar.tsx index 1b475f6..a7c07e2 100644 --- a/resources/js/components/app-sidebar.tsx +++ b/resources/js/components/app-sidebar.tsx @@ -6,10 +6,22 @@ import { IconMessageDots, IconUsers, } from '@tabler/icons-react'; +import { + BookOpen, + Building2, + Calendar, + ClipboardList, + FileText, + GraduationCap, + School, + User, + Users, +} from 'lucide-react'; import * as React from 'react'; import AppLogoIcon from '@/components/app-logo-icon'; -import { NavMain, type NavGroup, type NavItem } from '@/components/nav-main'; +import { NavMain } from '@/components/nav-main'; +import type { NavGroup, NavItem } from '@/components/nav-main'; import { NavSecondary } from '@/components/nav-secondary'; import { Sidebar, @@ -19,24 +31,15 @@ import { SidebarMenuButton, SidebarMenuItem, } from '@/components/ui/sidebar'; +import { index as assignmentsRoute } from '@/routes/admin/manage/assignments'; import { index as courseClassesRoute } from '@/routes/admin/manage/course-classes'; import { index as coursesRoute } from '@/routes/admin/manage/courses'; import { index as materialsRoute } from '@/routes/admin/manage/materials'; import { index as academicTerm } from '@/routes/admin/master/academic-terms'; import { index as departmentsRoute } from '@/routes/admin/master/departments'; +import { index as administratorsRoute } from '@/routes/admin/users/administrators'; import { index as lecturersRoute } from '@/routes/admin/users/lecturers'; import { index as studentsRoute } from '@/routes/admin/users/students'; -import { index as administratorsRoute } from '@/routes/admin/users/administrators'; -import { - BookOpen, - Building2, - Calendar, - FileText, - GraduationCap, - School, - User, - Users, -} from 'lucide-react'; const data: { navMain: (NavGroup | NavItem)[]; @@ -86,6 +89,11 @@ const data: { url: materialsRoute.url(), icon: FileText, }, + { + name: 'Tugas', + url: assignmentsRoute.url(), + icon: ClipboardList, + }, ], }, { diff --git a/resources/js/components/date-picker.tsx b/resources/js/components/date-picker.tsx index ab651e4..e049319 100644 --- a/resources/js/components/date-picker.tsx +++ b/resources/js/components/date-picker.tsx @@ -48,7 +48,6 @@ export function DatePicker({ defaultMonth={value ?? undefined} captionLayout="dropdown" onSelect={onChange} - initialFocus /> diff --git a/resources/js/components/datetime-field.tsx b/resources/js/components/datetime-field.tsx new file mode 100644 index 0000000..48a9839 --- /dev/null +++ b/resources/js/components/datetime-field.tsx @@ -0,0 +1,67 @@ +import { format } from 'date-fns'; +import { useState } from 'react'; +import { DatePicker } from '@/components/date-picker'; +import InputError from '@/components/input-error'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; + +type DateTimeFieldProps = { + label: string; + name: string; + required?: boolean; + defaultValue?: string | null; + error?: string; + placeholder?: string; +}; + +function parseDefault(value?: string | null): Date | undefined { + if (!value) { + return undefined; + } + + const date = new Date(value); + + return Number.isNaN(date.getTime()) ? undefined : date; +} + +export function DateTimeField({ + label, + name, + required, + defaultValue, + error, + placeholder = 'Pilih tanggal', +}: DateTimeFieldProps) { + const initial = parseDefault(defaultValue); + const [date, setDate] = useState(initial); + const [time, setTime] = useState(initial ? format(initial, 'HH:mm') : ''); + + const combined = date + ? `${format(date, 'yyyy-MM-dd')} ${time || '00:00'}:00` + : ''; + + return ( +
+ + +
+ + setTime(e.target.value)} + className="w-28" + /> +
+ +
+ ); +} diff --git a/resources/js/components/ui/calendar.tsx b/resources/js/components/ui/calendar.tsx index 5d31419..be12f05 100644 --- a/resources/js/components/ui/calendar.tsx +++ b/resources/js/components/ui/calendar.tsx @@ -1,5 +1,3 @@ -"use client" - import * as React from "react" import { ChevronDownIcon, diff --git a/resources/js/pages/admin/manage/assignments/columns.tsx b/resources/js/pages/admin/manage/assignments/columns.tsx new file mode 100644 index 0000000..df49eb4 --- /dev/null +++ b/resources/js/pages/admin/manage/assignments/columns.tsx @@ -0,0 +1,130 @@ +import type { ColumnDef } from '@tanstack/react-table'; +import { format } from 'date-fns'; +import { ClipboardList, Paperclip, Pencil, Trash2 } from 'lucide-react'; +import { RowActions } from '@/components/row-actions'; +import { index as submissionsIndex } from '@/routes/admin/manage/assignments/submissions'; +import type { Assignment } from '@/types/assignment'; + +export type { Assignment } from '@/types/assignment'; + +type CreateColumnsParams = { + handleEdit: (assignment: Assignment) => void; + handleDeleteClick: (assignment: Assignment) => void; +}; + +export function createAssignmentColumns( + params: CreateColumnsParams, +): ColumnDef[] { + const { handleEdit, handleDeleteClick } = params; + + return [ + { + accessorKey: 'title', + header: () => Judul, + cell: ({ row }) => ( + + {row.getValue('title') as string} + + ), + }, + { + accessorKey: 'course_class.class_name', + header: () => Kelas, + cell: ({ row }) => { + const courseClass = row.original.course_class; + + if (!courseClass) { + return '-'; + } + + const label = courseClass.class_name + ? `${courseClass.class_name} - ` + : ''; + + return `${label}${courseClass.course?.code ?? ''} ${courseClass.course?.name ?? ''}`; + }, + }, + { + accessorKey: 'deadline', + header: () => Batas Waktu, + cell: ({ row }) => { + const deadline = row.getValue('deadline') as string; + + return format(new Date(deadline), 'd MMM yyyy, HH:mm'); + }, + }, + { + accessorKey: 'attachment_name', + header: () => Lampiran, + cell: ({ row }) => { + const assignment = row.original; + + if (!assignment.attachment_url) { + return -; + } + + return ( + + + {assignment.attachment_name} + + ); + }, + }, + { + accessorKey: 'submissions_count', + header: () => ( + Pengumpulan + ), + meta: { + className: 'w-[120px] text-center', + headerClassName: 'w-[120px] text-center', + }, + cell: ({ row }) => ( +
+ {row.original.submissions_count} +
+ ), + }, + { + id: 'actions', + header: () => Aksi, + meta: { + className: 'w-[130px] text-center', + headerClassName: 'w-[130px] text-center', + }, + cell: ({ row }) => { + const assignment = row.original; + + return ( + , + href: submissionsIndex.url(assignment.id), + }, + { + label: 'Edit', + icon: , + onClick: () => handleEdit(assignment), + }, + { + label: 'Hapus', + icon: ( + + ), + onClick: () => handleDeleteClick(assignment), + }, + ]} + /> + ); + }, + }, + ]; +} diff --git a/resources/js/pages/admin/manage/assignments/index.tsx b/resources/js/pages/admin/manage/assignments/index.tsx new file mode 100644 index 0000000..9dbd7fa --- /dev/null +++ b/resources/js/pages/admin/manage/assignments/index.tsx @@ -0,0 +1,363 @@ +import { Head, router } from '@inertiajs/react'; +import { Plus } from 'lucide-react'; +import { useState } from 'react'; +import type { PaginationState } from '@/components/data-table'; +import { DataTable } from '@/components/data-table'; +import { DateTimeField } from '@/components/datetime-field'; +import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog'; +import { FileUploadField } from '@/components/file-upload-field'; +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 { 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 { useServerTable } from '@/hooks/use-server-table'; +import { + index as assignmentIndex, + destroy, + store, + update, +} from '@/routes/admin/manage/assignments'; +import type { Assignment } from '@/types/assignment'; +import { createAssignmentColumns } from './columns'; + +type CourseClassOption = { + id: number; + class_name: string | null; + course: { id: number; code: string; name: string } | null; +}; + +type Props = { + assignments: { + data: Assignment[]; + current_page: number; + last_page: number; + per_page: number; + total: number; + }; + courseClasses: CourseClassOption[]; + highlight?: number; +}; + +function courseClassLabel(courseClass: CourseClassOption): string { + const namePart = courseClass.class_name + ? `${courseClass.class_name} - ` + : ''; + + return `${namePart}${courseClass.course?.code ?? ''} ${courseClass.course?.name ?? ''}`; +} + +export default function AssignmentIndex({ + assignments, + courseClasses, + highlight, +}: Props) { + const [createOpen, setCreateOpen] = useState(false); + const [editing, setEditing] = useState(null); + const [deleting, setDeleting] = useState(null); + + const pagination: PaginationState = { + current_page: assignments.current_page, + last_page: assignments.last_page, + per_page: assignments.per_page, + total: assignments.total, + }; + + const { + search, + handlePageChange, + handlePerPageChange, + handleSearchChange, + } = useServerTable({ + route: () => assignmentIndex.url(), + pagination, + }); + + function handleDelete() { + if (!deleting) { + return; + } + + router.delete(destroy(deleting.id), { + onSuccess: () => setDeleting(null), + }); + } + + const columns = createAssignmentColumns({ + handleEdit: (assignment) => setEditing(assignment), + handleDeleteClick: (assignment) => setDeleting(assignment), + }); + + return ( + <> + + +
+ + Menampilkan tugas dari notifikasi. + +

+ ) + } + actions={ + + + } + /> + + + + { + if (!open) { + setEditing(null); + } + }} + editing={editing} + courseClasses={courseClasses} + /> + + + + { + 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[]; +}) { + return ( + onOpenChange(false)} + > + {({ errors }) => ( +
+
+ + + + +
+
+ + + +
+
+ +