diff --git a/app/Enums/RegistrationStatus.php b/app/Enums/RegistrationStatus.php new file mode 100644 index 0000000..89501db --- /dev/null +++ b/app/Enums/RegistrationStatus.php @@ -0,0 +1,19 @@ + 'Diajukan', + self::Approved => 'Disetujui', + self::Rejected => 'Ditolak', + }; + } +} diff --git a/app/Http/Controllers/Admin/Manage/CourseRegistrationController.php b/app/Http/Controllers/Admin/Manage/CourseRegistrationController.php new file mode 100644 index 0000000..4806ecd --- /dev/null +++ b/app/Http/Controllers/Admin/Manage/CourseRegistrationController.php @@ -0,0 +1,63 @@ + $this->service->paginated(...$request->validatedWithDefaults()), + 'students' => $this->studentService->getAllForSelect(), + 'academicTerms' => $this->academicTermService->getAll(), + 'courseClasses' => $this->courseClassService->getAllForSelect(), + 'lecturers' => $this->lecturerService->getAllForSelect(), + ]); + } + + public function store(CourseRegistrationRequest $request): RedirectResponse + { + $this->service->create($request->validated()); + + Inertia::flash('toast', ['type' => 'success', 'message' => 'Registrasi KRS berhasil ditambahkan.']); + + return to_route('admin.manage.course-registrations.index'); + } + + public function update(CourseRegistrationRequest $request, CourseRegistration $courseRegistration): RedirectResponse + { + $this->service->update($courseRegistration, $request->validated()); + + Inertia::flash('toast', ['type' => 'success', 'message' => 'Registrasi KRS berhasil diperbarui.']); + + return to_route('admin.manage.course-registrations.index'); + } + + public function destroy(CourseRegistration $courseRegistration): RedirectResponse + { + $this->service->delete($courseRegistration); + + return Inertia::flash('toast', ['type' => 'success', 'message' => 'Registrasi KRS berhasil dihapus.'])->back(); + } +} diff --git a/app/Http/Requests/Admin/Manage/CourseRegistrationRequest.php b/app/Http/Requests/Admin/Manage/CourseRegistrationRequest.php new file mode 100644 index 0000000..fc45fff --- /dev/null +++ b/app/Http/Requests/Admin/Manage/CourseRegistrationRequest.php @@ -0,0 +1,49 @@ +route('course_registration'); + + return [ + 'student_id' => ['required', 'integer', Rule::exists('students', 'id')], + 'academic_term_id' => ['required', 'integer', Rule::exists('academic_terms', 'id')], + 'course_class_id' => [ + 'required', + 'integer', + Rule::exists('course_classes', 'id'), + Rule::unique('course_registrations') + ->where('student_id', $this->input('student_id')) + ->ignore($registration?->id), + ], + 'status' => ['nullable', 'string', Rule::in(array_column(RegistrationStatus::cases(), 'value'))], + 'approved_by' => ['nullable', 'integer', Rule::exists('lecturers', 'id')], + ]; + } + + public function attributes(): array + { + return [ + 'course_class_id' => 'kelas mata kuliah', + ]; + } + + public function messages(): array + { + return [ + 'course_class_id.unique' => 'Mahasiswa ini sudah terdaftar pada kelas mata kuliah tersebut.', + ]; + } +} diff --git a/app/Models/CourseClass.php b/app/Models/CourseClass.php index 7d8c572..44a3374 100644 --- a/app/Models/CourseClass.php +++ b/app/Models/CourseClass.php @@ -61,4 +61,9 @@ public function attendances(): HasMany { return $this->hasMany(Attendance::class); } + + public function registrations(): HasMany + { + return $this->hasMany(CourseRegistration::class); + } } diff --git a/app/Models/CourseRegistration.php b/app/Models/CourseRegistration.php new file mode 100644 index 0000000..03f1e0b --- /dev/null +++ b/app/Models/CourseRegistration.php @@ -0,0 +1,42 @@ + RegistrationStatus::class, + ]; + } + + public function student(): BelongsTo + { + return $this->belongsTo(Student::class); + } + + public function academicTerm(): BelongsTo + { + return $this->belongsTo(AcademicTerm::class); + } + + public function courseClass(): BelongsTo + { + return $this->belongsTo(CourseClass::class); + } + + public function approver(): BelongsTo + { + return $this->belongsTo(Lecturer::class, 'approved_by'); + } +} diff --git a/app/Models/Student.php b/app/Models/Student.php index 7e1d64f..7b42ace 100644 --- a/app/Models/Student.php +++ b/app/Models/Student.php @@ -56,4 +56,9 @@ public function tuitionInvoices(): HasMany { return $this->hasMany(TuitionInvoice::class); } + + public function courseRegistrations(): HasMany + { + return $this->hasMany(CourseRegistration::class); + } } diff --git a/app/Services/Admin/Manage/CourseRegistrationService.php b/app/Services/Admin/Manage/CourseRegistrationService.php new file mode 100644 index 0000000..7484d3f --- /dev/null +++ b/app/Services/Admin/Manage/CourseRegistrationService.php @@ -0,0 +1,79 @@ +select(['id', 'student_id', 'academic_term_id', 'course_class_id', 'status', 'approved_by', 'created_at']) + ->with([ + 'student.user.profile', + 'student.department', + 'academicTerm:id,name,semester,start_date,end_date', + 'courseClass.course:id,code,name', + 'approver.user.profile', + ]) + ->when($search, fn ($q) => $q->whereHas('student', fn ($q) => $q->where('student_number', 'like', "%{$search}%") + ->orWhereHas('user.profile', fn ($q) => $q->where('full_name', 'like', "%{$search}%")))) + ->orderBy($sort, $direction) + ->paginate($perPage); + } + + public function create(array $data): CourseRegistration + { + $registration = CourseRegistration::create([ + 'student_id' => $data['student_id'], + 'academic_term_id' => $data['academic_term_id'], + 'course_class_id' => $data['course_class_id'], + 'status' => $data['status'] ?? RegistrationStatus::Submitted->value, + 'approved_by' => $data['approved_by'] ?? null, + ]); + + $this->syncEnrollment($registration); + + return $registration; + } + + public function update(CourseRegistration $registration, array $data): CourseRegistration + { + $registration->student_id = $data['student_id']; + $registration->academic_term_id = $data['academic_term_id']; + $registration->course_class_id = $data['course_class_id']; + $registration->status = $data['status'] ?? RegistrationStatus::Submitted->value; + $registration->approved_by = $data['approved_by'] ?? null; + $registration->update(); + + $this->syncEnrollment($registration); + + return $registration; + } + + public function delete(CourseRegistration $registration): bool + { + return $registration->delete(); + } + + private function syncEnrollment(CourseRegistration $registration): void + { + if ($registration->status !== RegistrationStatus::Approved) { + return; + } + + ClassEnrollment::firstOrCreate( + [ + 'course_class_id' => $registration->course_class_id, + 'student_id' => $registration->student_id, + ], + [ + 'enrolled_at' => now(), + ], + ); + } +} diff --git a/database/migrations/2026_08_25_000001_create_course_registrations_table.php b/database/migrations/2026_08_25_000001_create_course_registrations_table.php new file mode 100644 index 0000000..bcb99cf --- /dev/null +++ b/database/migrations/2026_08_25_000001_create_course_registrations_table.php @@ -0,0 +1,29 @@ +id(); + $table->foreignId('student_id')->constrained()->cascadeOnDelete(); + $table->foreignId('academic_term_id')->constrained()->cascadeOnDelete(); + $table->foreignId('course_class_id')->constrained()->cascadeOnDelete(); + $table->enum('status', array_values(RegistrationStatus::cases()))->nullable()->default(RegistrationStatus::Submitted->value); + $table->foreignId('approved_by')->nullable()->constrained('lecturers')->nullOnDelete(); + $table->timestamps(); + + $table->unique(['student_id', 'course_class_id']); + }); + } + + public function down(): void + { + Schema::dropIfExists('course_registrations'); + } +}; diff --git a/database/seeders/CourseRegistrationSeeder.php b/database/seeders/CourseRegistrationSeeder.php new file mode 100644 index 0000000..64d20aa --- /dev/null +++ b/database/seeders/CourseRegistrationSeeder.php @@ -0,0 +1,33 @@ + 1, + 'academic_term_id' => 2, + 'course_class_id' => 1, + 'status' => 'approved', + 'approved_by' => 1, + 'created_at' => '2026-01-20 08:00:00', + 'updated_at' => '2026-01-20 13:00:00', + ], + [ + 'student_id' => 2, + 'academic_term_id' => 2, + 'course_class_id' => 2, + 'status' => 'submitted', + 'approved_by' => null, + 'created_at' => '2026-01-20 08:10:00', + 'updated_at' => '2026-01-20 08:10:00', + ], + ]); + } +} diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index cfb29eb..1caaf11 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -20,6 +20,7 @@ public function run(): void CourseSeeder::class, CourseClassSeeder::class, ClassEnrollmentSeeder::class, + CourseRegistrationSeeder::class, MaterialSeeder::class, AssignmentSeeder::class, SubmissionSeeder::class, diff --git a/resources/js/components/app-sidebar.tsx b/resources/js/components/app-sidebar.tsx index 73e4a39..b427dd2 100644 --- a/resources/js/components/app-sidebar.tsx +++ b/resources/js/components/app-sidebar.tsx @@ -13,6 +13,7 @@ import { CalendarClock, ClipboardCheck, ClipboardList, + FileCheck2, FileText, GraduationCap, Receipt, @@ -37,6 +38,7 @@ import { import { index as assignmentsRoute } from '@/routes/admin/manage/assignments'; import { index as attendancesRoute } from '@/routes/admin/manage/attendances'; import { index as courseClassesRoute } from '@/routes/admin/manage/course-classes'; +import { index as courseRegistrationsRoute } from '@/routes/admin/manage/course-registrations'; import { index as coursesRoute } from '@/routes/admin/manage/courses'; import { index as materialsRoute } from '@/routes/admin/manage/materials'; import { index as schedulesRoute } from '@/routes/admin/manage/schedules'; @@ -90,6 +92,11 @@ const data: { { label: 'Kelas', items: [ + { + name: 'Registrasi KRS', + url: courseRegistrationsRoute.url(), + icon: FileCheck2, + }, { name: 'Materi', url: materialsRoute.url(), diff --git a/resources/js/pages/admin/manage/course-registrations/columns.tsx b/resources/js/pages/admin/manage/course-registrations/columns.tsx new file mode 100644 index 0000000..fad4ecc --- /dev/null +++ b/resources/js/pages/admin/manage/course-registrations/columns.tsx @@ -0,0 +1,141 @@ +import type { ColumnDef } from '@tanstack/react-table'; +import { format } from 'date-fns'; +import { Pencil, Trash2 } from 'lucide-react'; +import { RowActions } from '@/components/row-actions'; +import { Badge } from '@/components/ui/badge'; +import type { + CourseRegistration, + RegistrationStatus, +} from '@/types/course-registration'; +import { RegistrationStatusLabels } from '@/types/course-registration'; + +export type { CourseRegistration } from '@/types/course-registration'; + +type CreateColumnsParams = { + handleEdit: (registration: CourseRegistration) => void; + handleDeleteClick: (registration: CourseRegistration) => void; +}; + +export function createCourseRegistrationColumns( + params: CreateColumnsParams, +): ColumnDef[] { + const { handleEdit, handleDeleteClick } = params; + + return [ + { + accessorKey: 'student.student_number', + header: () => Mahasiswa, + cell: ({ row }) => { + const student = row.original.student; + + return ( +
+

+ {student?.user?.profile?.full_name ?? 'N/A'} +

+

+ {student?.student_number} ·{' '} + {student?.department?.name ?? '-'} +

+
+ ); + }, + }, + { + accessorKey: 'course_class.course.name', + header: () => Kelas Mata Kuliah, + cell: ({ row }) => { + const courseClass = row.original.course_class; + + return ( +
+

+ {courseClass?.course?.name ?? 'N/A'} +

+

+ {courseClass?.course?.code} + {courseClass?.class_name + ? ` ยท ${courseClass.class_name}` + : ''} +

+
+ ); + }, + }, + { + accessorKey: 'academic_term.name', + header: () => Periode, + cell: ({ row }) => row.original.academic_term?.name ?? '-', + }, + { + accessorKey: 'status', + header: () => Status, + meta: { + className: 'w-[130px] text-center', + headerClassName: 'w-[130px] text-center', + }, + cell: ({ row }) => { + const status = row.getValue( + 'status', + ) as RegistrationStatus | null; + + const variant = + status === 'approved' + ? 'default' + : status === 'rejected' + ? 'destructive' + : 'outline'; + + return ( +
+ {status ? ( + + {RegistrationStatusLabels[status]} + + ) : ( + - + )} +
+ ); + }, + }, + { + accessorKey: 'approver.user.profile.full_name', + header: () => Disetujui Oleh, + cell: ({ row }) => + row.original.approver?.user?.profile?.full_name ?? '-', + }, + { + accessorKey: 'created_at', + header: () => Diajukan, + cell: ({ row }) => + format(new Date(row.original.created_at), 'd MMM yyyy, HH:mm'), + }, + { + id: 'actions', + header: () => Aksi, + meta: { + className: 'w-[100px] text-center', + headerClassName: 'w-[100px] text-center', + }, + cell: ({ row }) => ( + , + onClick: () => handleEdit(row.original), + }, + { + label: 'Hapus', + icon: ( + + ), + onClick: () => handleDeleteClick(row.original), + }, + ]} + /> + ), + }, + ]; +} diff --git a/resources/js/pages/admin/manage/course-registrations/index.tsx b/resources/js/pages/admin/manage/course-registrations/index.tsx new file mode 100644 index 0000000..990dd86 --- /dev/null +++ b/resources/js/pages/admin/manage/course-registrations/index.tsx @@ -0,0 +1,428 @@ +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 { DeleteConfirmDialog } from '@/components/delete-confirm-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 { Label } from '@/components/ui/label'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { useServerTable } from '@/hooks/use-server-table'; +import { + index as courseRegistrationIndex, + destroy, + store, + update, +} from '@/routes/admin/manage/course-registrations'; +import type { + CourseRegistration, + CourseRegistrationCourseClass, + CourseRegistrationStudent, +} from '@/types/course-registration'; +import { + RegistrationStatuses, + RegistrationStatusLabels, +} from '@/types/course-registration'; +import { createCourseRegistrationColumns } from './columns'; + +type AcademicTermOption = { id: number; name: string; semester: string }; +type LecturerOption = { + id: number; + lecturer_number: string; + user: { profile: { full_name: string } | null } | null; +}; + +type Props = { + registrations: { + data: CourseRegistration[]; + current_page: number; + last_page: number; + per_page: number; + total: number; + }; + students: CourseRegistrationStudent[]; + academicTerms: AcademicTermOption[]; + courseClasses: CourseRegistrationCourseClass[]; + lecturers: LecturerOption[]; + highlight?: number; +}; + +function studentLabel(student: CourseRegistrationStudent): string { + return `${student.user?.profile?.full_name ?? 'N/A'} - ${student.student_number}`; +} + +function courseClassLabel(courseClass: CourseRegistrationCourseClass): string { + const course = courseClass.course; + const suffix = courseClass.class_name ? ` (${courseClass.class_name})` : ''; + + return `${course?.code ?? '-'} - ${course?.name ?? 'N/A'}${suffix}`; +} + +function lecturerLabel(lecturer: LecturerOption): string { + return `${lecturer.user?.profile?.full_name ?? 'N/A'} - ${lecturer.lecturer_number}`; +} + +export default function CourseRegistrationIndex({ + registrations, + students, + academicTerms, + courseClasses, + lecturers, + highlight, +}: Props) { + const [createOpen, setCreateOpen] = useState(false); + const [editing, setEditing] = useState(null); + const [deleting, setDeleting] = useState(null); + + const pagination: PaginationState = { + current_page: registrations.current_page, + last_page: registrations.last_page, + per_page: registrations.per_page, + total: registrations.total, + }; + + const { + search, + handlePageChange, + handlePerPageChange, + handleSearchChange, + } = useServerTable({ + route: () => courseRegistrationIndex.url(), + pagination, + }); + + function handleDelete() { + if (!deleting) { + return; + } + + router.delete(destroy(deleting.id), { + onSuccess: () => setDeleting(null), + }); + } + + const columns = createCourseRegistrationColumns({ + handleEdit: (registration) => setEditing(registration), + handleDeleteClick: (registration) => setDeleting(registration), + }); + + return ( + <> + + +
+ + Menampilkan registrasi dari notifikasi. +

+ ) + } + actions={ + + + } + /> + + + + { + if (!open) { + setEditing(null); + } + }} + editing={editing} + students={students} + academicTerms={academicTerms} + courseClasses={courseClasses} + lecturers={lecturers} + /> + + + + { + if (!open) { + setDeleting(null); + } + }} + title="Hapus Registrasi" + description={(registration) => + `Apakah Anda yakin ingin menghapus registrasi KRS untuk "${registration.student?.user?.profile?.full_name ?? 'mahasiswa ini'}"? Tindakan ini tidak dapat dibatalkan.` + } + onConfirm={handleDelete} + /> +
+ + ); +} + +function RegistrationFields({ + errors, + editing, + students, + academicTerms, + courseClasses, + lecturers, +}: { + errors: Record; + editing?: CourseRegistration; + students: CourseRegistrationStudent[]; + academicTerms: AcademicTermOption[]; + courseClasses: CourseRegistrationCourseClass[]; + lecturers: LecturerOption[]; +}) { + return ( + <> +
+ + {!editing && } + + +
+
+ + {!editing && } + + +
+
+ + {!editing && } + + +
+
+ + + + +
+
+ + + + +
+ + ); +} + +function CreateForm({ + open, + onOpenChange, + students, + academicTerms, + courseClasses, + lecturers, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; + students: CourseRegistrationStudent[]; + academicTerms: AcademicTermOption[]; + courseClasses: CourseRegistrationCourseClass[]; + lecturers: LecturerOption[]; +}) { + return ( + onOpenChange(false)} + > + {({ errors }) => ( +
+ +
+ )} +
+ ); +} + +function EditForm({ + open, + onOpenChange, + editing, + students, + academicTerms, + courseClasses, + lecturers, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; + editing: CourseRegistration | null; + students: CourseRegistrationStudent[]; + academicTerms: AcademicTermOption[]; + courseClasses: CourseRegistrationCourseClass[]; + lecturers: LecturerOption[]; +}) { + return ( + onOpenChange(false)} + > + {({ errors }) => + editing && ( +
+ +
+ ) + } +
+ ); +} diff --git a/resources/js/types/course-registration.ts b/resources/js/types/course-registration.ts new file mode 100644 index 0000000..027e8b8 --- /dev/null +++ b/resources/js/types/course-registration.ts @@ -0,0 +1,46 @@ +export const RegistrationStatuses = [ + 'submitted', + 'approved', + 'rejected', +] as const; + +export type RegistrationStatus = (typeof RegistrationStatuses)[number]; + +export const RegistrationStatusLabels: Record = { + submitted: 'Diajukan', + approved: 'Disetujui', + rejected: 'Ditolak', +}; + +export type CourseRegistrationStudent = { + id: number; + student_number: string; + department: { id: number; name: string } | null; + user: { profile: { full_name: string } | null } | null; +}; + +export type CourseRegistrationCourseClass = { + id: number; + class_name: string | null; + course: { id: number; code: string; name: string } | null; +}; + +export type CourseRegistrationApprover = { + id: number; + user: { profile: { full_name: string } | null } | null; +}; + +export type CourseRegistration = { + id: number; + student_id: number; + student: CourseRegistrationStudent | null; + academic_term_id: number; + academic_term: { id: number; name: string; semester: string } | null; + course_class_id: number; + course_class: CourseRegistrationCourseClass | null; + status: RegistrationStatus | null; + approved_by: number | null; + approver: CourseRegistrationApprover | null; + created_at: string; + updated_at: string; +}; diff --git a/routes/admin.php b/routes/admin.php index d981550..c1aac1d 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -5,6 +5,7 @@ use App\Http\Controllers\Admin\Manage\ClassEnrollmentController; use App\Http\Controllers\Admin\Manage\CourseClassController; use App\Http\Controllers\Admin\Manage\CourseController; +use App\Http\Controllers\Admin\Manage\CourseRegistrationController; use App\Http\Controllers\Admin\Manage\MaterialController; use App\Http\Controllers\Admin\Manage\ScheduleController; use App\Http\Controllers\Admin\Manage\SubmissionController; @@ -33,6 +34,8 @@ Route::delete('{enrollment}', [ClassEnrollmentController::class, 'destroy'])->name('destroy'); }); + Route::resource('course-registrations', CourseRegistrationController::class)->except(['create', 'edit', 'show']); + Route::resource('materials', MaterialController::class)->except(['create', 'edit', 'show']); Route::resource('assignments', AssignmentController::class)->except(['create', 'edit', 'show']); diff --git a/storage/framework/lsp-fe894e15fb6bf7ea.php b/storage/framework/lsp-fe894e15fb6bf7ea.php new file mode 100644 index 0000000..bd07085 --- /dev/null +++ b/storage/framework/lsp-fe894e15fb6bf7ea.php @@ -0,0 +1,157 @@ +hasDefaultValue()) { +return ['default' => $property->getDefaultValue()]; +} + +if ($parameter?->isDefaultValueAvailable()) { +return ['default' => $parameter->getDefaultValue()]; +} + +return []; +} + +public static function formatDefaultValue(mixed $value): mixed +{ +return match (true) { +is_array($value) => 'array(...)', +$value instanceof UnitEnum => get_class($value) . '::' . $value->name, +$value instanceof Closure => 'Closure', +is_object($value) => get_class($value), +is_string($value) => var_export($value, true), +is_null($value) => 'null', +is_bool($value) => $value ? 'true' : 'false', +default => $value, +}; +} +} + +use Illuminate\Routing\Route; +use Illuminate\Routing\UrlGenerator; +use Illuminate\Support\Facades\Artisan; +use Laravel\Folio\FolioManager; +use Symfony\Component\Console\Output\BufferedOutput; + +$routes = new class +{ +public function all() +{ +return collect(app('router')->getRoutes()->getRoutes()) +->map(fn (Route $route) => $this->getRoute($route)) +->merge($this->getFolioRoutes()); +} + +protected function getFolioRoutes() +{ +try { +$output = new BufferedOutput; + +Artisan::call('folio:list', ['--json' => true], $output); + +$mountPaths = collect(app(FolioManager::class)->mountPaths()); + +return collect(json_decode($output->fetch(), true))->map(fn ($route) => $this->getFolioRoute($route, $mountPaths)); +} catch (Exception|Throwable $e) { +return []; +} +} + +protected function getFolioRoute($route, $mountPaths) +{ +if ($mountPaths->count() === 1) { +$mountPath = $mountPaths[0]; +} else { +$mountPath = $mountPaths->first(fn ($mp) => file_exists($mp->path . DIRECTORY_SEPARATOR . $route['view'])); +} + +$path = $route['view']; + +if ($mountPath) { +$path = $mountPath->path . DIRECTORY_SEPARATOR . $path; +} + +return [ +'method' => $route['method'], +'uri' => $route['uri'], +'name' => $route['name'], +'action' => null, +'parameters' => [], +'filename' => LspHelper::relativePath($path), +'line' => 0, +]; +} + +protected function getRoute(Route $route) +{ +try { +$reflection = $this->getRouteReflection($route); +} catch (Throwable $e) { +$reflection = null; +} + +return [ +'method' => collect($route->methods()) +->filter(fn ($method) => $method !== 'HEAD') +->implode('|'), +'uri' => $route->uri(), +'name' => $route->getName(), +'action' => $route->getActionName(), +'parameters' => $route->parameterNames(), +'filename' => $reflection ? LspHelper::relativePath($reflection->getFileName()) : null, +'line' => $reflection ? $reflection->getStartLine() : null, +'livewire' => $this->getLivewireView($route), +]; +} + +protected function getLivewireView(Route $route): ?string +{ +if ($route->getActionName() !== 'Livewire\Features\SupportRouting\LivewirePageController') { +return null; +} + +return $route->defaults['_livewire_component'] ?? null; +} + +protected function getRouteReflection(Route $route) +{ +if ($route->getActionName() === 'Closure') { +return new ReflectionFunction($route->getAction()['uses']); +} + +if (!str_contains($route->getActionName(), '@')) { +return new ReflectionClass($route->getActionName()); +} + +try { +return new ReflectionMethod($route->getControllerClass(), $route->getActionMethod()); +} catch (Throwable $e) { +$namespace = app(UrlGenerator::class)->getRootControllerNamespace() +?? (app()->getNamespace() . 'Http\Controllers'); + +return new ReflectionMethod( +$namespace . '\\' . ltrim($route->getControllerClass(), '\\'), +$route->getActionMethod(), +); +} +} +}; + +echo $routes->all()->toJson();