diff --git a/app/Http/Controllers/Admin/AcademicClasses/AssignmentController.php b/app/Http/Controllers/Admin/AcademicClasses/AssignmentController.php index 55024f8..646845f 100644 --- a/app/Http/Controllers/Admin/AcademicClasses/AssignmentController.php +++ b/app/Http/Controllers/Admin/AcademicClasses/AssignmentController.php @@ -23,10 +23,11 @@ public function index(PaginatedRequest $request): Response { return Inertia::render('admin/academic-classes/assignments/index', [ 'assignments' => $this->service->paginated( + $request->user(), ...$request->validatedWithDefaults(), courseClassId: $request->validated('course_class_id'), ), - 'courseClasses' => $this->courseClassService->getAllForSelect(), + 'courseClasses' => $this->courseClassService->getAllForSelect($request->user()), 'filters' => $request->only(['course_class_id']), ]); } diff --git a/app/Http/Requests/Admin/AcademicClasses/AssignmentRequest.php b/app/Http/Requests/Admin/AcademicClasses/AssignmentRequest.php index 1bf1eb1..7517482 100644 --- a/app/Http/Requests/Admin/AcademicClasses/AssignmentRequest.php +++ b/app/Http/Requests/Admin/AcademicClasses/AssignmentRequest.php @@ -14,8 +14,16 @@ public function authorize(): bool public function rules(): array { + $user = $this->user(); + + $courseClassRule = Rule::exists('course_classes', 'id'); + + if ($user->hasRole('dosen')) { + $courseClassRule->where('lecturer_id', $user->lecturer?->id); + } + return [ - 'course_class_id' => ['required', 'integer', Rule::exists('course_classes', 'id')], + 'course_class_id' => ['required', 'integer', $courseClassRule], 'title' => ['required', 'string', 'max:150'], 'description' => ['nullable', 'string'], 'deadline' => ['required', 'date'], diff --git a/app/Services/Admin/AcademicClasses/AssignmentService.php b/app/Services/Admin/AcademicClasses/AssignmentService.php index cb9e5c5..a15bd0c 100644 --- a/app/Services/Admin/AcademicClasses/AssignmentService.php +++ b/app/Services/Admin/AcademicClasses/AssignmentService.php @@ -2,13 +2,15 @@ namespace App\Services\Admin\AcademicClasses; +use App\Enums\RegistrationStatus; use App\Models\Assignment; +use App\Models\User; use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Http\UploadedFile; class AssignmentService { - public function paginated(int $perPage = 25, string $search = '', ?int $courseClassId = null): LengthAwarePaginator + public function paginated(User $user, int $perPage = 25, string $search = '', ?int $courseClassId = null): LengthAwarePaginator { return Assignment::query() ->select(['id', 'course_class_id', 'title', 'description', 'deadline']) @@ -16,6 +18,13 @@ public function paginated(int $perPage = 25, string $search = '', ?int $courseCl ->with('courseClass.course:id,code,name') ->when($search, fn ($q) => $q->where('title', 'like', "%{$search}%")) ->when($courseClassId, fn ($q) => $q->where('course_class_id', $courseClassId)) + ->when($user->hasRole('dosen'), fn ($q) => $q->whereHas('courseClass', fn ($q) => $q->where('lecturer_id', $user->lecturer?->id))) + ->when($user->hasRole('mahasiswa'), fn ($q) => $q->whereHas('courseClass', function ($q) use ($user) { + $q->whereHas('registrations', function ($q) use ($user) { + $q->where('student_id', $user->student?->id) + ->whereHas('submission', fn ($q) => $q->where('status', RegistrationStatus::Approved)); + }); + })) ->latest() ->paginate($perPage); } diff --git a/database/seeders/RolePermissionSeeder.php b/database/seeders/RolePermissionSeeder.php index 6d9ed40..eacbf0c 100644 --- a/database/seeders/RolePermissionSeeder.php +++ b/database/seeders/RolePermissionSeeder.php @@ -48,6 +48,7 @@ public function run(): void 'delete-letter-requests', 'view-schedules', 'view-materials', + 'view-assignments', ...$feedbackSelfService, ], 'dosen' => [ @@ -63,7 +64,10 @@ public function run(): void 'create-materials', 'update-materials', 'delete-materials', - ...$feedbackSelfService, + 'view-assignments', + 'create-assignments', + 'update-assignments', + 'delete-assignments', ...$feedbackSelfService, ], 'staff-admin' => [ diff --git a/resources/js/pages/admin/academic-classes/assignments/columns.tsx b/resources/js/pages/admin/academic-classes/assignments/columns.tsx index 37337d7..212ea87 100644 --- a/resources/js/pages/admin/academic-classes/assignments/columns.tsx +++ b/resources/js/pages/admin/academic-classes/assignments/columns.tsx @@ -1,6 +1,7 @@ import type { ColumnDef } from '@tanstack/react-table'; import { format } from 'date-fns'; -import { ClipboardList, Paperclip, Pencil, Trash2 } from 'lucide-react'; +import { ClipboardList, Pencil, Trash2 } from 'lucide-react'; +import { AttachmentPreviewDialog } from '@/components/attachment-preview-dialog'; import { RowActions } from '@/components/row-actions'; import { index as submissionsIndex } from '@/routes/admin/academic-classes/assignments/submissions'; import type { Assignment } from '@/types/assignment'; @@ -64,20 +65,15 @@ export function createAssignmentColumns( cell: ({ row }) => { const assignment = row.original; - if (!assignment.attachment_url) { + if (!assignment.attachment_url || !assignment.attachment_name) { return -; } return ( - - - {assignment.attachment_name} - + ); }, }, diff --git a/resources/js/pages/admin/academic-classes/assignments/index.tsx b/resources/js/pages/admin/academic-classes/assignments/index.tsx index 5924b90..160790c 100644 --- a/resources/js/pages/admin/academic-classes/assignments/index.tsx +++ b/resources/js/pages/admin/academic-classes/assignments/index.tsx @@ -6,21 +6,25 @@ 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 type { FilterField } from '@/components/filter-dialog'; +import type { 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 { Button } from '@/components/ui/button'; +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'; @@ -35,9 +39,17 @@ import { createAssignmentColumns } from './columns'; type CourseClassOption = { id: number; - course: { id: number; code: string; name: string } | null; + 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 Props = { assignments: { data: Assignment[]; @@ -54,7 +66,80 @@ type Props = { }; function courseClassLabel(courseClass: CourseClassOption): string { - return `${courseClass.course?.code ?? ''} ${courseClass.course?.name ?? ''}`; + return `${courseClass.course?.code ?? ''} - ${courseClass.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 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({ @@ -72,14 +157,12 @@ export default function AssignmentIndex({ const canDelete = hasPermission('delete-assignments'); const canViewSubmissions = hasPermission('view-assignment-submissions'); - const filterFields: FilterField[] = [ + const filterFields = [ { key: 'course_class_id', label: 'Kelas', - options: courseClasses.map((courseClass) => ({ - value: String(courseClass.id), - label: courseClassLabel(courseClass), - })), + type: 'combobox' as const, + groups: courseClassFilterGroups(courseClasses), }, ]; @@ -227,6 +310,10 @@ function CreateForm({ onOpenChange: (open: boolean) => void; courseClasses: CourseClassOption[]; }) { + const [courseClass, setCourseClass] = useState( + null, + ); + return ( onOpenChange(false)} + onSuccess={() => { + onOpenChange(false); + setCourseClass(null); + }} > {({ errors }) => (
@@ -242,22 +332,16 @@ function CreateForm({ - - + +
@@ -310,6 +394,13 @@ function EditForm({ editing: Assignment | null; courseClasses: CourseClassOption[]; }) { + const [courseClass, setCourseClass] = useState( + editing + ? (courseClasses.find((c) => c.id === editing.course_class_id) ?? + null) + : null, + ); + return ( * - + value={courseClass?.id ?? ''} + /> +