feat: add course registration management functionality with CRUD operations
Some checks failed
tests / ci (pull_request) Has been cancelled
Some checks failed
tests / ci (pull_request) Has been cancelled
This commit is contained in:
parent
4ff18c23c5
commit
2f23747781
19
app/Enums/RegistrationStatus.php
Normal file
19
app/Enums/RegistrationStatus.php
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
enum RegistrationStatus: string
|
||||||
|
{
|
||||||
|
case Submitted = 'submitted';
|
||||||
|
case Approved = 'approved';
|
||||||
|
case Rejected = 'rejected';
|
||||||
|
|
||||||
|
public function label(): string
|
||||||
|
{
|
||||||
|
return match ($this) {
|
||||||
|
self::Submitted => 'Diajukan',
|
||||||
|
self::Approved => 'Disetujui',
|
||||||
|
self::Rejected => 'Ditolak',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin\Manage;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\Admin\Manage\CourseRegistrationRequest;
|
||||||
|
use App\Http\Requests\PaginatedRequest;
|
||||||
|
use App\Models\CourseRegistration;
|
||||||
|
use App\Services\Admin\Manage\CourseClassService;
|
||||||
|
use App\Services\Admin\Manage\CourseRegistrationService;
|
||||||
|
use App\Services\Admin\Master\AcademicTermService;
|
||||||
|
use App\Services\Admin\Users\LecturerService;
|
||||||
|
use App\Services\Admin\Users\StudentService;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
use Inertia\Response;
|
||||||
|
|
||||||
|
class CourseRegistrationController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly CourseRegistrationService $service,
|
||||||
|
private readonly StudentService $studentService,
|
||||||
|
private readonly AcademicTermService $academicTermService,
|
||||||
|
private readonly CourseClassService $courseClassService,
|
||||||
|
private readonly LecturerService $lecturerService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function index(PaginatedRequest $request): Response
|
||||||
|
{
|
||||||
|
return Inertia::render('admin/manage/course-registrations/index', [
|
||||||
|
'registrations' => $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();
|
||||||
|
}
|
||||||
|
}
|
||||||
49
app/Http/Requests/Admin/Manage/CourseRegistrationRequest.php
Normal file
49
app/Http/Requests/Admin/Manage/CourseRegistrationRequest.php
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Admin\Manage;
|
||||||
|
|
||||||
|
use App\Enums\RegistrationStatus;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class CourseRegistrationRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
$registration = $this->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.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -61,4 +61,9 @@ public function attendances(): HasMany
|
|||||||
{
|
{
|
||||||
return $this->hasMany(Attendance::class);
|
return $this->hasMany(Attendance::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function registrations(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(CourseRegistration::class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
42
app/Models/CourseRegistration.php
Normal file
42
app/Models/CourseRegistration.php
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Enums\RegistrationStatus;
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
#[Guarded(['id'])]
|
||||||
|
class CourseRegistration extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'status' => 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -56,4 +56,9 @@ public function tuitionInvoices(): HasMany
|
|||||||
{
|
{
|
||||||
return $this->hasMany(TuitionInvoice::class);
|
return $this->hasMany(TuitionInvoice::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function courseRegistrations(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(CourseRegistration::class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
79
app/Services/Admin/Manage/CourseRegistrationService.php
Normal file
79
app/Services/Admin/Manage/CourseRegistrationService.php
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Admin\Manage;
|
||||||
|
|
||||||
|
use App\Enums\RegistrationStatus;
|
||||||
|
use App\Models\ClassEnrollment;
|
||||||
|
use App\Models\CourseRegistration;
|
||||||
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||||
|
|
||||||
|
class CourseRegistrationService
|
||||||
|
{
|
||||||
|
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
|
||||||
|
{
|
||||||
|
return CourseRegistration::query()
|
||||||
|
->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(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Enums\RegistrationStatus;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('course_registrations', function (Blueprint $table) {
|
||||||
|
$table->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');
|
||||||
|
}
|
||||||
|
};
|
||||||
33
database/seeders/CourseRegistrationSeeder.php
Normal file
33
database/seeders/CourseRegistrationSeeder.php
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Models\CourseRegistration;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
|
class CourseRegistrationSeeder extends Seeder
|
||||||
|
{
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
CourseRegistration::insert([
|
||||||
|
[
|
||||||
|
'student_id' => 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',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -20,6 +20,7 @@ public function run(): void
|
|||||||
CourseSeeder::class,
|
CourseSeeder::class,
|
||||||
CourseClassSeeder::class,
|
CourseClassSeeder::class,
|
||||||
ClassEnrollmentSeeder::class,
|
ClassEnrollmentSeeder::class,
|
||||||
|
CourseRegistrationSeeder::class,
|
||||||
MaterialSeeder::class,
|
MaterialSeeder::class,
|
||||||
AssignmentSeeder::class,
|
AssignmentSeeder::class,
|
||||||
SubmissionSeeder::class,
|
SubmissionSeeder::class,
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import {
|
|||||||
CalendarClock,
|
CalendarClock,
|
||||||
ClipboardCheck,
|
ClipboardCheck,
|
||||||
ClipboardList,
|
ClipboardList,
|
||||||
|
FileCheck2,
|
||||||
FileText,
|
FileText,
|
||||||
GraduationCap,
|
GraduationCap,
|
||||||
Receipt,
|
Receipt,
|
||||||
@ -37,6 +38,7 @@ import {
|
|||||||
import { index as assignmentsRoute } from '@/routes/admin/manage/assignments';
|
import { index as assignmentsRoute } from '@/routes/admin/manage/assignments';
|
||||||
import { index as attendancesRoute } from '@/routes/admin/manage/attendances';
|
import { index as attendancesRoute } from '@/routes/admin/manage/attendances';
|
||||||
import { index as courseClassesRoute } from '@/routes/admin/manage/course-classes';
|
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 coursesRoute } from '@/routes/admin/manage/courses';
|
||||||
import { index as materialsRoute } from '@/routes/admin/manage/materials';
|
import { index as materialsRoute } from '@/routes/admin/manage/materials';
|
||||||
import { index as schedulesRoute } from '@/routes/admin/manage/schedules';
|
import { index as schedulesRoute } from '@/routes/admin/manage/schedules';
|
||||||
@ -90,6 +92,11 @@ const data: {
|
|||||||
{
|
{
|
||||||
label: 'Kelas',
|
label: 'Kelas',
|
||||||
items: [
|
items: [
|
||||||
|
{
|
||||||
|
name: 'Registrasi KRS',
|
||||||
|
url: courseRegistrationsRoute.url(),
|
||||||
|
icon: FileCheck2,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Materi',
|
name: 'Materi',
|
||||||
url: materialsRoute.url(),
|
url: materialsRoute.url(),
|
||||||
|
|||||||
141
resources/js/pages/admin/manage/course-registrations/columns.tsx
Normal file
141
resources/js/pages/admin/manage/course-registrations/columns.tsx
Normal file
@ -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<CourseRegistration>[] {
|
||||||
|
const { handleEdit, handleDeleteClick } = params;
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
accessorKey: 'student.student_number',
|
||||||
|
header: () => <span>Mahasiswa</span>,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const student = row.original.student;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">
|
||||||
|
{student?.user?.profile?.full_name ?? 'N/A'}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{student?.student_number} ·{' '}
|
||||||
|
{student?.department?.name ?? '-'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'course_class.course.name',
|
||||||
|
header: () => <span>Kelas Mata Kuliah</span>,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const courseClass = row.original.course_class;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">
|
||||||
|
{courseClass?.course?.name ?? 'N/A'}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{courseClass?.course?.code}
|
||||||
|
{courseClass?.class_name
|
||||||
|
? ` · ${courseClass.class_name}`
|
||||||
|
: ''}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'academic_term.name',
|
||||||
|
header: () => <span>Periode</span>,
|
||||||
|
cell: ({ row }) => row.original.academic_term?.name ?? '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'status',
|
||||||
|
header: () => <span className="block text-center">Status</span>,
|
||||||
|
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 (
|
||||||
|
<div className="flex justify-center">
|
||||||
|
{status ? (
|
||||||
|
<Badge variant={variant}>
|
||||||
|
{RegistrationStatusLabels[status]}
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<span className="text-muted-foreground">-</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'approver.user.profile.full_name',
|
||||||
|
header: () => <span>Disetujui Oleh</span>,
|
||||||
|
cell: ({ row }) =>
|
||||||
|
row.original.approver?.user?.profile?.full_name ?? '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'created_at',
|
||||||
|
header: () => <span>Diajukan</span>,
|
||||||
|
cell: ({ row }) =>
|
||||||
|
format(new Date(row.original.created_at), 'd MMM yyyy, HH:mm'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'actions',
|
||||||
|
header: () => <span className="block text-center">Aksi</span>,
|
||||||
|
meta: {
|
||||||
|
className: 'w-[100px] text-center',
|
||||||
|
headerClassName: 'w-[100px] text-center',
|
||||||
|
},
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<RowActions
|
||||||
|
actions={[
|
||||||
|
{
|
||||||
|
label: 'Edit',
|
||||||
|
icon: <Pencil className="h-4 w-4" />,
|
||||||
|
onClick: () => handleEdit(row.original),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Hapus',
|
||||||
|
icon: (
|
||||||
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
|
),
|
||||||
|
onClick: () => handleDeleteClick(row.original),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
428
resources/js/pages/admin/manage/course-registrations/index.tsx
Normal file
428
resources/js/pages/admin/manage/course-registrations/index.tsx
Normal file
@ -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<CourseRegistration | null>(null);
|
||||||
|
const [deleting, setDeleting] = useState<CourseRegistration | null>(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 (
|
||||||
|
<>
|
||||||
|
<Head title="Registrasi KRS" />
|
||||||
|
|
||||||
|
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-auto p-4 md:p-6">
|
||||||
|
<PageHeader
|
||||||
|
title="Registrasi KRS"
|
||||||
|
description={
|
||||||
|
highlight && (
|
||||||
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
|
Menampilkan registrasi dari notifikasi.
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
actions={
|
||||||
|
<Button asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setCreateOpen(true)}
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
Tambah
|
||||||
|
</button>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CreateForm
|
||||||
|
open={createOpen}
|
||||||
|
onOpenChange={setCreateOpen}
|
||||||
|
students={students}
|
||||||
|
academicTerms={academicTerms}
|
||||||
|
courseClasses={courseClasses}
|
||||||
|
lecturers={lecturers}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EditForm
|
||||||
|
key={editing?.id}
|
||||||
|
open={editing !== null}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
setEditing(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
editing={editing}
|
||||||
|
students={students}
|
||||||
|
academicTerms={academicTerms}
|
||||||
|
courseClasses={courseClasses}
|
||||||
|
lecturers={lecturers}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DataTable
|
||||||
|
columns={columns}
|
||||||
|
data={registrations.data}
|
||||||
|
emptyText="Belum ada data registrasi KRS."
|
||||||
|
pagination={pagination}
|
||||||
|
onPageChange={handlePageChange}
|
||||||
|
onPerPageChange={handlePerPageChange}
|
||||||
|
onSearchChange={handleSearchChange}
|
||||||
|
searchValue={search}
|
||||||
|
searchKey="student"
|
||||||
|
searchPlaceholder="Cari nama atau NIM mahasiswa..."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DeleteConfirmDialog
|
||||||
|
target={deleting}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function RegistrationFields({
|
||||||
|
errors,
|
||||||
|
editing,
|
||||||
|
students,
|
||||||
|
academicTerms,
|
||||||
|
courseClasses,
|
||||||
|
lecturers,
|
||||||
|
}: {
|
||||||
|
errors: Record<string, string>;
|
||||||
|
editing?: CourseRegistration;
|
||||||
|
students: CourseRegistrationStudent[];
|
||||||
|
academicTerms: AcademicTermOption[];
|
||||||
|
courseClasses: CourseRegistrationCourseClass[];
|
||||||
|
lecturers: LecturerOption[];
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label>
|
||||||
|
Mahasiswa <span className="text-destructive">*</span>
|
||||||
|
</Label>
|
||||||
|
{!editing && <input type="hidden" name="student_id" />}
|
||||||
|
<Select
|
||||||
|
name="student_id"
|
||||||
|
defaultValue={
|
||||||
|
editing ? String(editing.student_id) : undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue placeholder="Pilih mahasiswa" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{students.map((student) => (
|
||||||
|
<SelectItem
|
||||||
|
key={student.id}
|
||||||
|
value={String(student.id)}
|
||||||
|
>
|
||||||
|
{studentLabel(student)}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<InputError message={errors.student_id} />
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label>
|
||||||
|
Kelas Mata Kuliah{' '}
|
||||||
|
<span className="text-destructive">*</span>
|
||||||
|
</Label>
|
||||||
|
{!editing && <input type="hidden" name="course_class_id" />}
|
||||||
|
<Select
|
||||||
|
name="course_class_id"
|
||||||
|
defaultValue={
|
||||||
|
editing ? String(editing.course_class_id) : undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue placeholder="Pilih kelas mata kuliah" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{courseClasses.map((courseClass) => (
|
||||||
|
<SelectItem
|
||||||
|
key={courseClass.id}
|
||||||
|
value={String(courseClass.id)}
|
||||||
|
>
|
||||||
|
{courseClassLabel(courseClass)}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<InputError message={errors.course_class_id} />
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label>
|
||||||
|
Periode Akademik <span className="text-destructive">*</span>
|
||||||
|
</Label>
|
||||||
|
{!editing && <input type="hidden" name="academic_term_id" />}
|
||||||
|
<Select
|
||||||
|
name="academic_term_id"
|
||||||
|
defaultValue={
|
||||||
|
editing ? String(editing.academic_term_id) : undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue placeholder="Pilih periode akademik" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{academicTerms.map((term) => (
|
||||||
|
<SelectItem key={term.id} value={String(term.id)}>
|
||||||
|
{term.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<InputError message={errors.academic_term_id} />
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label>Status</Label>
|
||||||
|
<input type="hidden" name="status" />
|
||||||
|
<Select
|
||||||
|
name="status"
|
||||||
|
defaultValue={editing?.status ?? 'submitted'}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue placeholder="Pilih status" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{RegistrationStatuses.map((status) => (
|
||||||
|
<SelectItem key={status} value={status}>
|
||||||
|
{RegistrationStatusLabels[status]}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<InputError message={errors.status} />
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label>Disetujui Oleh</Label>
|
||||||
|
<input type="hidden" name="approved_by" />
|
||||||
|
<Select
|
||||||
|
name="approved_by"
|
||||||
|
defaultValue={
|
||||||
|
editing?.approved_by
|
||||||
|
? String(editing.approved_by)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue placeholder="Pilih dosen (jika disetujui)" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{lecturers.map((lecturer) => (
|
||||||
|
<SelectItem
|
||||||
|
key={lecturer.id}
|
||||||
|
value={String(lecturer.id)}
|
||||||
|
>
|
||||||
|
{lecturerLabel(lecturer)}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<InputError message={errors.approved_by} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CreateForm({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
students,
|
||||||
|
academicTerms,
|
||||||
|
courseClasses,
|
||||||
|
lecturers,
|
||||||
|
}: {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
students: CourseRegistrationStudent[];
|
||||||
|
academicTerms: AcademicTermOption[];
|
||||||
|
courseClasses: CourseRegistrationCourseClass[];
|
||||||
|
lecturers: LecturerOption[];
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<FormDialog
|
||||||
|
open={open}
|
||||||
|
onOpenChange={onOpenChange}
|
||||||
|
title="Tambah Registrasi KRS"
|
||||||
|
action={store()}
|
||||||
|
resetOnSuccess
|
||||||
|
onSuccess={() => onOpenChange(false)}
|
||||||
|
>
|
||||||
|
{({ errors }) => (
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<RegistrationFields
|
||||||
|
errors={errors}
|
||||||
|
students={students}
|
||||||
|
academicTerms={academicTerms}
|
||||||
|
courseClasses={courseClasses}
|
||||||
|
lecturers={lecturers}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</FormDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<FormDialog
|
||||||
|
open={open}
|
||||||
|
onOpenChange={onOpenChange}
|
||||||
|
title="Edit Registrasi KRS"
|
||||||
|
action={editing ? update(editing.id) : ''}
|
||||||
|
resetOnSuccess
|
||||||
|
onSuccess={() => onOpenChange(false)}
|
||||||
|
>
|
||||||
|
{({ errors }) =>
|
||||||
|
editing && (
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<RegistrationFields
|
||||||
|
errors={errors}
|
||||||
|
editing={editing}
|
||||||
|
students={students}
|
||||||
|
academicTerms={academicTerms}
|
||||||
|
courseClasses={courseClasses}
|
||||||
|
lecturers={lecturers}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</FormDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
46
resources/js/types/course-registration.ts
Normal file
46
resources/js/types/course-registration.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
export const RegistrationStatuses = [
|
||||||
|
'submitted',
|
||||||
|
'approved',
|
||||||
|
'rejected',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type RegistrationStatus = (typeof RegistrationStatuses)[number];
|
||||||
|
|
||||||
|
export const RegistrationStatusLabels: Record<RegistrationStatus, string> = {
|
||||||
|
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;
|
||||||
|
};
|
||||||
@ -5,6 +5,7 @@
|
|||||||
use App\Http\Controllers\Admin\Manage\ClassEnrollmentController;
|
use App\Http\Controllers\Admin\Manage\ClassEnrollmentController;
|
||||||
use App\Http\Controllers\Admin\Manage\CourseClassController;
|
use App\Http\Controllers\Admin\Manage\CourseClassController;
|
||||||
use App\Http\Controllers\Admin\Manage\CourseController;
|
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\MaterialController;
|
||||||
use App\Http\Controllers\Admin\Manage\ScheduleController;
|
use App\Http\Controllers\Admin\Manage\ScheduleController;
|
||||||
use App\Http\Controllers\Admin\Manage\SubmissionController;
|
use App\Http\Controllers\Admin\Manage\SubmissionController;
|
||||||
@ -33,6 +34,8 @@
|
|||||||
Route::delete('{enrollment}', [ClassEnrollmentController::class, 'destroy'])->name('destroy');
|
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('materials', MaterialController::class)->except(['create', 'edit', 'show']);
|
||||||
|
|
||||||
Route::resource('assignments', AssignmentController::class)->except(['create', 'edit', 'show']);
|
Route::resource('assignments', AssignmentController::class)->except(['create', 'edit', 'show']);
|
||||||
|
|||||||
157
storage/framework/lsp-fe894e15fb6bf7ea.php
Normal file
157
storage/framework/lsp-fe894e15fb6bf7ea.php
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
<?php
|
||||||
|
error_reporting(error_reporting() & ~(E_WARNING | E_CORE_WARNING | E_COMPILE_WARNING | E_USER_WARNING | E_DEPRECATED | E_USER_DEPRECATED));
|
||||||
|
class LspHelper
|
||||||
|
{
|
||||||
|
public static function relativePath($path)
|
||||||
|
{
|
||||||
|
if (!str_contains($path, base_path())) {
|
||||||
|
return (string) $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ltrim(str_replace(base_path(), '', realpath($path) ?: $path), DIRECTORY_SEPARATOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function isVendor($path)
|
||||||
|
{
|
||||||
|
return str_contains($path, base_path('vendor'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function propertyDefault(ReflectionProperty $property, ?ReflectionParameter $parameter = null): array
|
||||||
|
{
|
||||||
|
if ($property->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();
|
||||||
Loading…
Reference in New Issue
Block a user