diff --git a/app/Http/Controllers/Admin/Manage/CourseRegistrationController.php b/app/Http/Controllers/Admin/Manage/CourseRegistrationController.php index 89148a8..a46818a 100644 --- a/app/Http/Controllers/Admin/Manage/CourseRegistrationController.php +++ b/app/Http/Controllers/Admin/Manage/CourseRegistrationController.php @@ -2,10 +2,12 @@ namespace App\Http\Controllers\Admin\Manage; +use App\Enums\UserRole; use App\Http\Controllers\Controller; -use App\Http\Requests\Admin\Manage\RejectCourseRegistrationRequest; -use App\Http\Requests\Admin\Manage\SaveCourseRegistrationRequest; -use App\Http\Requests\Admin\Manage\SignCourseRegistrationRequest; +use App\Http\Requests\Admin\Manage\CourseRegistration\RejectCourseRegistrationRequest; +use App\Http\Requests\Admin\Manage\CourseRegistration\SignAsAdvisorCourseRegistrationRequest; +use App\Http\Requests\Admin\Manage\CourseRegistration\SignAsDepartmentLeaderCourseRegistrationRequest; +use App\Http\Requests\Admin\Manage\CourseRegistration\UpdateCourseRegistrationRequest; use App\Http\Requests\PaginatedRequest; use App\Models\Course; use App\Models\CourseRegistrationSubmission; @@ -25,21 +27,18 @@ public function __construct( public function index(PaginatedRequest $request): Response|RedirectResponse { - $user = auth()->user(); + $user = $request->user(); - if ($user->hasRole('mahasiswa')) { + if ($user->hasRole(UserRole::Mahasiswa->value)) { return to_route('admin.manage.course-registrations.show', $user->student->id); } - $isKaprodi = $user->hasRole('kaprodi'); - return Inertia::render('admin/manage/course-registrations/index', [ 'departments' => $this->service->departmentSummary(), 'registrations' => $this->service->paginated( + $user, ...$request->validatedWithDefaults(), departmentId: $request->validated('department_id'), - advisorLecturerId: ! $isKaprodi && $user->hasRole('dosen') ? $user->lecturer?->id : null, - ledDepartmentIds: $isKaprodi ? $user->ledDepartmentIds() : null, ), 'filters' => $request->only(['department_id']), ]); @@ -47,8 +46,8 @@ public function index(PaginatedRequest $request): Response|RedirectResponse public function show(Student $student): Response { - $user = auth()->user(); - $isStudent = $user->hasRole('mahasiswa'); + $user = request()->user(); + $isStudent = $user->hasRole(UserRole::Mahasiswa->value); abort_if($isStudent && $user->student->id !== $student->id, 403); abort_if(! $isStudent && ! $user->canAccessCourseRegistrationOf($student), 403); @@ -61,7 +60,7 @@ public function show(Student $student): Response && $user->can('approve-course-registrations') && $user->canReviewCourseRegistrationOf($student); - $canSignAsKaprodi = $user->isKaprodiOf($student); + $canSignAsDepartmentLeader = $user->isDepartmentLeaderOf($student); $canSignAsAdvisor = $user->isAdvisorOf($student); $activeTerm = $this->academicTermService->getActive(); @@ -97,12 +96,12 @@ public function show(Student $student): Response 'hasActiveTerm' => $activeTerm !== null, 'backHref' => $backHref, 'canReview' => $canReview, - 'canSignAsKaprodi' => $canSignAsKaprodi, + 'canSignAsDepartmentLeader' => $canSignAsDepartmentLeader, 'canSignAsAdvisor' => $canSignAsAdvisor, ]); } - public function save(Student $student, string $semester, SaveCourseRegistrationRequest $request): RedirectResponse + public function update(Student $student, string $semester, UpdateCourseRegistrationRequest $request): RedirectResponse { $semesterNumber = CourseRegistrationSubmission::parseSemesterKey($semester); @@ -138,23 +137,19 @@ public function save(Student $student, string $semester, SaveCourseRegistrationR return to_route('admin.manage.course-registrations.show', $student->id); } - public function signAsKaprodi(Student $student, string $semester, SignCourseRegistrationRequest $request): RedirectResponse + public function signAsDepartmentLeader(Student $student, string $semester, SignAsDepartmentLeaderCourseRegistrationRequest $request): RedirectResponse { - abort_unless(auth()->user()->isKaprodiOf($student), 403); - $submission = $this->service->findOrCreateForSemester($student, $semester); - $this->service->signAsKaprodi($submission, $request->file('signature')); + $this->service->signAsDepartmentLeader($submission, $request->file('signature')); Inertia::flash('toast', ['type' => 'success', 'message' => 'Tanda tangan berhasil disimpan.']); return to_route('admin.manage.course-registrations.show', $student->id); } - public function signAsAdvisor(Student $student, string $semester, SignCourseRegistrationRequest $request): RedirectResponse + public function signAsAdvisor(Student $student, string $semester, SignAsAdvisorCourseRegistrationRequest $request): RedirectResponse { - abort_unless(auth()->user()->isAdvisorOf($student), 403); - $submission = $this->service->findOrCreateForSemester($student, $semester); $this->service->signAsAdvisor($submission, $request->file('signature')); @@ -166,7 +161,11 @@ public function signAsAdvisor(Student $student, string $semester, SignCourseRegi public function approve(CourseRegistrationSubmission $submission): RedirectResponse { - abort_unless(auth()->user()->canReviewCourseRegistrationOf($submission->student), 403); + abort_unless( + request()->user()->can('approve-course-registrations') + && request()->user()->canReviewCourseRegistrationOf($submission->student), + 403 + ); $this->service->approve($submission); @@ -175,8 +174,6 @@ public function approve(CourseRegistrationSubmission $submission): RedirectRespo public function reject(RejectCourseRegistrationRequest $request, CourseRegistrationSubmission $submission): RedirectResponse { - abort_unless(auth()->user()->canReviewCourseRegistrationOf($submission->student), 403); - $this->service->reject($submission, $request->validated('reason')); return Inertia::flash('toast', ['type' => 'success', 'message' => 'KRS berhasil ditolak.'])->back(); diff --git a/app/Http/Requests/Admin/Manage/RejectCourseRegistrationRequest.php b/app/Http/Requests/Admin/Manage/CourseRegistration/RejectCourseRegistrationRequest.php similarity index 66% rename from app/Http/Requests/Admin/Manage/RejectCourseRegistrationRequest.php rename to app/Http/Requests/Admin/Manage/CourseRegistration/RejectCourseRegistrationRequest.php index 03a8ed3..81ecd6c 100644 --- a/app/Http/Requests/Admin/Manage/RejectCourseRegistrationRequest.php +++ b/app/Http/Requests/Admin/Manage/CourseRegistration/RejectCourseRegistrationRequest.php @@ -1,6 +1,6 @@ user()->can('reject-course-registrations'); + return $this->user()->can('reject-course-registrations') + && $this->user()->canReviewCourseRegistrationOf($this->route('submission')->student); } public function rules(): array @@ -17,11 +18,4 @@ public function rules(): array 'reason' => ['required', 'string', 'max:1000'], ]; } - - public function messages(): array - { - return [ - 'reason.required' => 'Alasan penolakan wajib diisi.', - ]; - } } diff --git a/app/Http/Requests/Admin/Manage/SignCourseRegistrationRequest.php b/app/Http/Requests/Admin/Manage/CourseRegistration/SignAsAdvisorCourseRegistrationRequest.php similarity index 56% rename from app/Http/Requests/Admin/Manage/SignCourseRegistrationRequest.php rename to app/Http/Requests/Admin/Manage/CourseRegistration/SignAsAdvisorCourseRegistrationRequest.php index ba18cac..d8db2a4 100644 --- a/app/Http/Requests/Admin/Manage/SignCourseRegistrationRequest.php +++ b/app/Http/Requests/Admin/Manage/CourseRegistration/SignAsAdvisorCourseRegistrationRequest.php @@ -1,14 +1,14 @@ user()->isAdvisorOf($this->route('student')); } public function rules(): array diff --git a/app/Http/Requests/Admin/Manage/CourseRegistration/SignAsDepartmentLeaderCourseRegistrationRequest.php b/app/Http/Requests/Admin/Manage/CourseRegistration/SignAsDepartmentLeaderCourseRegistrationRequest.php new file mode 100644 index 0000000..655a3da --- /dev/null +++ b/app/Http/Requests/Admin/Manage/CourseRegistration/SignAsDepartmentLeaderCourseRegistrationRequest.php @@ -0,0 +1,20 @@ +user()->isDepartmentLeaderOf($this->route('student')); + } + + public function rules(): array + { + return [ + 'signature' => ['required', 'file', 'image', 'max:2048'], + ]; + } +} diff --git a/app/Http/Requests/Admin/Manage/SaveCourseRegistrationRequest.php b/app/Http/Requests/Admin/Manage/CourseRegistration/UpdateCourseRegistrationRequest.php similarity index 68% rename from app/Http/Requests/Admin/Manage/SaveCourseRegistrationRequest.php rename to app/Http/Requests/Admin/Manage/CourseRegistration/UpdateCourseRegistrationRequest.php index 54580e7..a470a9e 100644 --- a/app/Http/Requests/Admin/Manage/SaveCourseRegistrationRequest.php +++ b/app/Http/Requests/Admin/Manage/CourseRegistration/UpdateCourseRegistrationRequest.php @@ -1,16 +1,17 @@ route('student'); - return $this->user()->hasRole('mahasiswa') + return $this->user()->hasRole(UserRole::Mahasiswa->value) && $this->user()->student?->id === $student?->id; } diff --git a/app/Models/CourseRegistrationSubmission.php b/app/Models/CourseRegistrationSubmission.php index 23414fb..c5c37c0 100644 --- a/app/Models/CourseRegistrationSubmission.php +++ b/app/Models/CourseRegistrationSubmission.php @@ -13,7 +13,7 @@ use Spatie\MediaLibrary\InteractsWithMedia; #[Guarded(['id'])] -#[Appends(['signature_url', 'kaprodi_signature_url', 'advisor_signature_url'])] +#[Appends(['signature_url', 'department_leader_signature_url', 'advisor_signature_url'])] class CourseRegistrationSubmission extends Model implements HasMedia { use InteractsWithMedia; @@ -31,7 +31,7 @@ protected function casts(): array public function registerMediaCollections(): void { $this->addMediaCollection('signature')->singleFile(); - $this->addMediaCollection('kaprodi_signature')->singleFile(); + $this->addMediaCollection('department_leader_signature')->singleFile(); $this->addMediaCollection('advisor_signature')->singleFile(); } @@ -67,10 +67,10 @@ protected function signatureUrl(): Attribute ); } - protected function kaprodiSignatureUrl(): Attribute + protected function departmentLeaderSignatureUrl(): Attribute { return Attribute::make( - get: fn () => $this->getFirstMediaUrl('kaprodi_signature') ?: null, + get: fn () => $this->getFirstMediaUrl('department_leader_signature') ?: null, ); } diff --git a/app/Models/User.php b/app/Models/User.php index 9992b48..0158541 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Enums\UserRole; use Illuminate\Database\Eloquent\Attributes\Appends; use Illuminate\Database\Eloquent\Attributes\Guarded; use Illuminate\Database\Eloquent\Attributes\Hidden; @@ -110,38 +111,38 @@ public function uploads(): HasMany public function isAdvisorOf(Student $student): bool { - return $this->hasRole('dosen') && $this->lecturer && $student->academic_advisor_id === $this->lecturer->id; + return $this->hasRole(UserRole::Dosen->value) && $this->lecturer && $student->academic_advisor_id === $this->lecturer->id; } - public function isKaprodiOf(Student $student): bool + public function isDepartmentLeaderOf(Student $student): bool { - return $this->hasRole('kaprodi') && $this->lecturer && $student->department?->currentLeader?->lecturer_id === $this->lecturer->id; + return $this->hasRole(UserRole::Kaprodi->value) && $this->lecturer && $student->department?->currentLeader?->lecturer_id === $this->lecturer->id; } /** - * Gates page access: the student's academic advisor, the kaprodi of - * their department, or staff roles may view a student's KRS. + * Gates page access: the student's academic advisor, the department + * leader of their department, or staff roles may view a student's KRS. */ public function canAccessCourseRegistrationOf(Student $student): bool { - if ($this->hasRole('dosen') || $this->hasRole('kaprodi')) { - return $this->isAdvisorOf($student) || $this->isKaprodiOf($student); + if ($this->hasRole(UserRole::Dosen->value) || $this->hasRole(UserRole::Kaprodi->value)) { + return $this->isAdvisorOf($student) || $this->isDepartmentLeaderOf($student); } return true; } /** - * Only the student's academic advisor may approve/reject; the kaprodi - * signs but does not decide. Other staff roles retain override access. + * Only the student's academic advisor may approve/reject; the department + * leader signs but does not decide. Other staff roles retain override access. */ public function canReviewCourseRegistrationOf(Student $student): bool { - if ($this->hasRole('dosen')) { + if ($this->hasRole(UserRole::Dosen->value)) { return $this->isAdvisorOf($student); } - if ($this->hasRole('kaprodi')) { + if ($this->hasRole(UserRole::Kaprodi->value)) { return false; } diff --git a/app/Services/Admin/Manage/CourseRegistrationService.php b/app/Services/Admin/Manage/CourseRegistrationService.php index 5d55813..aef3950 100644 --- a/app/Services/Admin/Manage/CourseRegistrationService.php +++ b/app/Services/Admin/Manage/CourseRegistrationService.php @@ -4,6 +4,7 @@ use App\Enums\RegistrationStatus; use App\Enums\StudentStatus; +use App\Enums\UserRole; use App\Models\AcademicTerm; use App\Models\ClassEnrollment; use App\Models\Course; @@ -12,6 +13,7 @@ use App\Models\CourseRegistrationSubmission; use App\Models\Department; use App\Models\Student; +use App\Models\User; use App\Services\Admin\Master\AcademicTermService; use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Http\UploadedFile; @@ -24,22 +26,12 @@ public function __construct( private readonly AcademicTermService $academicTermService, ) {} - /** - * @return Collection - */ - public function departmentSummary(): Collection + public function paginated(User $user, int $perPage = 25, string $search = '', ?int $departmentId = null): LengthAwarePaginator { - return Department::query() - ->select('id', 'name') - ->orderBy('name') - ->get(); - } + $isDepartmentLeader = $user->hasRole(UserRole::Kaprodi->value); + $advisorLecturerId = ! $isDepartmentLeader && $user->hasRole(UserRole::Dosen->value) ? $user->lecturer?->id : null; + $ledDepartmentIds = $isDepartmentLeader ? $user->ledDepartmentIds() : null; - /** - * @param array|null $ledDepartmentIds Restricts results to a kaprodi's own department(s). - */ - public function paginated(int $perPage = 25, string $search = '', ?int $departmentId = null, ?int $advisorLecturerId = null, ?array $ledDepartmentIds = null): LengthAwarePaginator - { $paginator = Student::query() ->select(['students.id', 'students.user_id', 'students.student_number', 'students.department_id']) ->join('departments', 'departments.id', '=', 'students.department_id') @@ -219,11 +211,11 @@ public function saveRegistrations( }); } - public function signAsKaprodi(CourseRegistrationSubmission $submission, UploadedFile $signature): CourseRegistrationSubmission + public function signAsDepartmentLeader(CourseRegistrationSubmission $submission, UploadedFile $signature): CourseRegistrationSubmission { $this->assertSignable($submission); - $submission->addMedia($signature)->toMediaCollection('kaprodi_signature'); + $submission->addMedia($signature)->toMediaCollection('department_leader_signature'); return $submission; } @@ -246,7 +238,7 @@ private function assertSignable(CourseRegistrationSubmission $submission): void public function approve(CourseRegistrationSubmission $submission): CourseRegistrationSubmission { abort_unless($submission->status === RegistrationStatus::Submitted, 403); - abort_if($submission->kaprodi_signature_url === null, 422, 'Ketua Program Studi belum menandatangani KRS ini.'); + abort_if($submission->department_leader_signature_url === null, 422, 'Ketua Program Studi belum menandatangani KRS ini.'); abort_if($submission->advisor_signature_url === null, 422, 'Silakan tanda tangani KRS ini terlebih dahulu.'); return DB::transaction(function () use ($submission) { @@ -299,4 +291,14 @@ public function reject(CourseRegistrationSubmission $submission, string $reason) return $submission; }); } + + /** + * @return Collection + */ + public function departmentSummary(): Collection + { + return Department::query() + ->orderBy('name') + ->get(['id', 'name']); + } } diff --git a/resources/js/pages/admin/manage/course-registrations/show.tsx b/resources/js/pages/admin/manage/course-registrations/show.tsx index 851adb8..09036c5 100644 --- a/resources/js/pages/admin/manage/course-registrations/show.tsx +++ b/resources/js/pages/admin/manage/course-registrations/show.tsx @@ -25,9 +25,9 @@ import { Textarea } from '@/components/ui/textarea'; import { advisor_signature, approve, - kaprodi_signature, + department_leader_signature, reject, - save, + update, } from '@/routes/admin/manage/course-registrations'; import type { CourseRegistrationSubmission, @@ -238,7 +238,7 @@ function SemesterCard({ isSemesterOpen, backHref, canReview, - canSignAsKaprodi, + canSignAsDepartmentLeader, canSignAsAdvisor, }: { semester: number | null; @@ -247,14 +247,14 @@ function SemesterCard({ isSemesterOpen: boolean; backHref: string | null; canReview: boolean; - canSignAsKaprodi: boolean; + canSignAsDepartmentLeader: boolean; canSignAsAdvisor: boolean; }) { const [rejecting, setRejecting] = useState(false); const student = submission.student; const department = student?.department ?? null; - const kaprodi = department?.current_leader?.lecturer ?? null; + const departmentLeader = department?.current_leader?.lecturer ?? null; const advisor = student?.academic_advisor ?? null; const isViewOnly = backHref !== null; @@ -267,11 +267,13 @@ function SemesterCard({ canReview && submission.status === 'submitted' && !notYetSubmitted; const canReviewersSign = submission.status === 'submitted' && !notYetSubmitted; - const missingKaprodiSignature = !submission.kaprodi_signature_url; + const missingDepartmentLeaderSignature = + !submission.department_leader_signature_url; const missingOwnSignature = - (canSignAsKaprodi && !submission.kaprodi_signature_url) || + (canSignAsDepartmentLeader && + !submission.department_leader_signature_url) || (canSignAsAdvisor && !submission.advisor_signature_url); - const cannotApprove = missingKaprodiSignature || missingOwnSignature; + const cannotApprove = missingDepartmentLeaderSignature || missingOwnSignature; const totalCredits = submission.course_registrations.reduce( (sum, registration) => @@ -291,7 +293,7 @@ function SemesterCard({ return (
@@ -471,25 +473,28 @@ function SemesterCard({

Ketua Program Studi,

- {kaprodi?.user?.profile + {departmentLeader?.user?.profile ?.full_name ?? '-'}

NIDN.{' '} - {kaprodi?.lecturer_number ?? '-'} + {departmentLeader?.lecturer_number ?? + '-'}

@@ -579,7 +584,7 @@ function SemesterCard({ {cannotApprove && (

- {missingKaprodiSignature + {missingDepartmentLeaderSignature ? 'Menunggu tanda tangan Ketua Program Studi sebelum dapat disetujui.' : 'Silakan tanda tangani KRS ini terlebih dahulu sebelum menyetujui.'}

@@ -628,7 +633,7 @@ type Props = { hasActiveTerm: boolean; backHref: string | null; canReview: boolean; - canSignAsKaprodi: boolean; + canSignAsDepartmentLeader: boolean; canSignAsAdvisor: boolean; }; @@ -640,7 +645,7 @@ export default function CourseRegistrationDetail({ hasActiveTerm, backHref, canReview, - canSignAsKaprodi, + canSignAsDepartmentLeader, canSignAsAdvisor, }: Props) { const [activeSemester, setActiveSemester] = useState(() => @@ -712,7 +717,7 @@ export default function CourseRegistrationDetail({ )} backHref={backHref} canReview={canReview} - canSignAsKaprodi={canSignAsKaprodi} + canSignAsDepartmentLeader={canSignAsDepartmentLeader} canSignAsAdvisor={canSignAsAdvisor} /> ) : ( diff --git a/resources/js/types/course-registration.ts b/resources/js/types/course-registration.ts index 306ae8d..77a81a6 100644 --- a/resources/js/types/course-registration.ts +++ b/resources/js/types/course-registration.ts @@ -78,7 +78,7 @@ export type CourseRegistrationSubmission = { rejection_reason: string | null; signed_at: string; signature_url: string | null; - kaprodi_signature_url: string | null; + department_leader_signature_url: string | null; advisor_signature_url: string | null; has_registrations: boolean; student: CourseRegistrationSubmissionStudent | null; diff --git a/routes/admin.php b/routes/admin.php index 773125d..1117ce0 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -107,8 +107,8 @@ Route::patch('submissions/{submission}/approve', [CourseRegistrationController::class, 'approve'])->name('approve')->middleware('permission:approve-course-registrations'); Route::patch('submissions/{submission}/reject', [CourseRegistrationController::class, 'reject'])->name('reject')->middleware('permission:reject-course-registrations'); Route::get('{student}', [CourseRegistrationController::class, 'show'])->name('show')->middleware('permission:view-course-registrations'); - Route::put('{student}/{semester}', [CourseRegistrationController::class, 'save'])->name('save')->middleware('permission:view-course-registrations'); - Route::patch('{student}/{semester}/kaprodi-signature', [CourseRegistrationController::class, 'signAsKaprodi'])->name('kaprodi_signature')->middleware('permission:view-course-registrations'); + Route::put('{student}/{semester}', [CourseRegistrationController::class, 'update'])->name('update')->middleware('permission:view-course-registrations'); + Route::patch('{student}/{semester}/department-leader-signature', [CourseRegistrationController::class, 'signAsDepartmentLeader'])->name('department_leader_signature')->middleware('permission:view-course-registrations'); Route::patch('{student}/{semester}/advisor-signature', [CourseRegistrationController::class, 'signAsAdvisor'])->name('advisor_signature')->middleware('permission:approve-course-registrations'); });