diff --git a/app/Http/Controllers/Admin/AcademicClasses/AssignmentController.php b/app/Http/Controllers/Admin/AcademicClasses/AssignmentController.php index b1e7c11..55024f8 100644 --- a/app/Http/Controllers/Admin/AcademicClasses/AssignmentController.php +++ b/app/Http/Controllers/Admin/AcademicClasses/AssignmentController.php @@ -22,8 +22,12 @@ public function __construct( public function index(PaginatedRequest $request): Response { return Inertia::render('admin/academic-classes/assignments/index', [ - 'assignments' => $this->service->paginated(...$request->validatedWithDefaults()), + 'assignments' => $this->service->paginated( + ...$request->validatedWithDefaults(), + courseClassId: $request->validated('course_class_id'), + ), 'courseClasses' => $this->courseClassService->getAllForSelect(), + 'filters' => $request->only(['course_class_id']), ]); } diff --git a/app/Http/Controllers/Admin/AcademicClasses/CourseRegistrationController.php b/app/Http/Controllers/Admin/AcademicClasses/CourseRegistrationController.php index f0687cc..be79253 100644 --- a/app/Http/Controllers/Admin/AcademicClasses/CourseRegistrationController.php +++ b/app/Http/Controllers/Admin/AcademicClasses/CourseRegistrationController.php @@ -28,11 +28,16 @@ public function __construct( public function index(PaginatedRequest $request): Response { return Inertia::render('admin/academic-classes/course-registrations/index', [ - 'registrations' => $this->service->paginated(...$request->validatedWithDefaults()), + 'registrations' => $this->service->paginated( + ...$request->validatedWithDefaults(), + status: $request->validated('status'), + academicTermId: $request->validated('academic_term_id'), + ), 'students' => $this->studentService->getAllForSelect(), 'academicTerms' => $this->academicTermService->getAllForSelect(), 'courseClasses' => $this->courseClassService->getAllForSelect(), 'lecturers' => $this->lecturerService->getAllForSelect(), + 'filters' => $request->only(['status', 'academic_term_id']), ]); } diff --git a/app/Http/Controllers/Admin/AcademicClasses/MaterialController.php b/app/Http/Controllers/Admin/AcademicClasses/MaterialController.php index 0962df3..c5148b0 100644 --- a/app/Http/Controllers/Admin/AcademicClasses/MaterialController.php +++ b/app/Http/Controllers/Admin/AcademicClasses/MaterialController.php @@ -22,8 +22,12 @@ public function __construct( public function index(PaginatedRequest $request): Response { return Inertia::render('admin/academic-classes/materials/index', [ - 'materials' => $this->service->paginated(...$request->validatedWithDefaults()), + 'materials' => $this->service->paginated( + ...$request->validatedWithDefaults(), + courseClassId: $request->validated('course_class_id'), + ), 'courseClasses' => $this->courseClassService->getAllForSelect(), + 'filters' => $request->only(['course_class_id']), ]); } diff --git a/app/Http/Controllers/Admin/Announcements/AnnouncementController.php b/app/Http/Controllers/Admin/Announcements/AnnouncementController.php index cc0f3f7..238dff1 100644 --- a/app/Http/Controllers/Admin/Announcements/AnnouncementController.php +++ b/app/Http/Controllers/Admin/Announcements/AnnouncementController.php @@ -22,8 +22,12 @@ public function __construct( public function index(PaginatedRequest $request): Response { return Inertia::render('admin/announcements/index', [ - 'announcements' => $this->service->paginated(...$request->validatedWithDefaults()), + 'announcements' => $this->service->paginated( + ...$request->validatedWithDefaults(), + departmentId: $request->validated('department_id'), + ), 'departments' => $this->departmentService->getAllForSelect(), + 'filters' => $request->only(['department_id']), ]); } diff --git a/app/Http/Controllers/Admin/FeedbackController.php b/app/Http/Controllers/Admin/FeedbackController.php index 23f1b69..3f1d632 100644 --- a/app/Http/Controllers/Admin/FeedbackController.php +++ b/app/Http/Controllers/Admin/FeedbackController.php @@ -2,6 +2,7 @@ namespace App\Http\Controllers\Admin; +use App\Enums\FeedbackStatus; use App\Enums\FeedbackType; use App\Http\Controllers\Controller; use App\Http\Requests\Admin\FeedbackRequest; @@ -22,8 +23,15 @@ public function __construct( public function index(PaginatedRequest $request): Response { return Inertia::render('admin/feedback/index', [ - 'feedbacks' => $this->service->paginated($request->user(), ...$request->validatedWithDefaults()), + 'feedbacks' => $this->service->paginated( + $request->user(), + ...$request->validatedWithDefaults(), + type: $request->validated('type'), + status: $request->validated('status'), + ), 'types' => FeedbackType::options(), + 'statuses' => FeedbackStatus::options(), + 'filters' => $request->only(['type', 'status']), ]); } diff --git a/app/Http/Controllers/Admin/Finances/TuitionInvoiceController.php b/app/Http/Controllers/Admin/Finances/TuitionInvoiceController.php index e73a043..e515f1d 100644 --- a/app/Http/Controllers/Admin/Finances/TuitionInvoiceController.php +++ b/app/Http/Controllers/Admin/Finances/TuitionInvoiceController.php @@ -24,9 +24,13 @@ public function __construct( public function index(PaginatedRequest $request): Response { return Inertia::render('admin/finances/tuition-invoices/index', [ - 'invoices' => $this->service->paginated(...$request->validatedWithDefaults()), + 'invoices' => $this->service->paginated( + ...$request->validatedWithDefaults(), + academicTermId: $request->validated('academic_term_id'), + ), 'students' => $this->studentService->getAllForSelect(), 'academicTerms' => $this->academicTermService->getAllForSelect(), + 'filters' => $request->only(['academic_term_id']), ]); } diff --git a/app/Http/Controllers/Admin/Manage/CourseClassController.php b/app/Http/Controllers/Admin/Manage/CourseClassController.php index abc2cda..068d46f 100644 --- a/app/Http/Controllers/Admin/Manage/CourseClassController.php +++ b/app/Http/Controllers/Admin/Manage/CourseClassController.php @@ -26,10 +26,15 @@ public function __construct( public function index(PaginatedRequest $request): Response { return Inertia::render('admin/manage/course-classes/index', [ - 'courseClasses' => $this->service->paginated(...$request->validatedWithDefaults()), + 'courseClasses' => $this->service->paginated( + ...$request->validatedWithDefaults(), + academicTermId: $request->validated('academic_term_id'), + method: $request->validated('method'), + ), 'courses' => $this->courseService->getAllForSelect(), 'lecturers' => $this->lecturerService->getAllForSelect(), 'academicTerms' => $this->academicTermService->getAllForSelect(), + 'filters' => $request->only(['academic_term_id', 'method']), ]); } diff --git a/app/Http/Controllers/Admin/Manage/CourseController.php b/app/Http/Controllers/Admin/Manage/CourseController.php index 2f38221..c3246c7 100644 --- a/app/Http/Controllers/Admin/Manage/CourseController.php +++ b/app/Http/Controllers/Admin/Manage/CourseController.php @@ -22,8 +22,12 @@ public function __construct( public function index(PaginatedRequest $request): Response { return Inertia::render('admin/manage/courses/index', [ - 'courses' => $this->service->paginated(...$request->validatedWithDefaults()), + 'courses' => $this->service->paginated( + ...$request->validatedWithDefaults(), + departmentId: $request->validated('department_id'), + ), 'departments' => $this->departmentService->getAllForSelect(), + 'filters' => $request->only(['department_id']), ]); } diff --git a/app/Http/Controllers/Admin/Master/AcademicTermController.php b/app/Http/Controllers/Admin/Master/AcademicTermController.php index 88be15b..dbcb771 100644 --- a/app/Http/Controllers/Admin/Master/AcademicTermController.php +++ b/app/Http/Controllers/Admin/Master/AcademicTermController.php @@ -20,7 +20,12 @@ public function __construct( public function index(PaginatedRequest $request): Response { return Inertia::render('admin/master/academic-terms/index', [ - 'academicTerms' => $this->service->paginated(...$request->validatedWithDefaults()), + 'academicTerms' => $this->service->paginated( + ...$request->validatedWithDefaults(), + semester: $request->validated('semester'), + isActive: $request->filled('is_active') ? filter_var($request->validated('is_active'), FILTER_VALIDATE_BOOLEAN) : null, + ), + 'filters' => $request->only(['semester', 'is_active']), ]); } diff --git a/app/Http/Controllers/Admin/Services/AcademicAdvisingLogController.php b/app/Http/Controllers/Admin/Services/AcademicAdvisingLogController.php index 3b37138..3a51511 100644 --- a/app/Http/Controllers/Admin/Services/AcademicAdvisingLogController.php +++ b/app/Http/Controllers/Admin/Services/AcademicAdvisingLogController.php @@ -24,9 +24,13 @@ public function __construct( public function index(PaginatedRequest $request): Response { return Inertia::render('admin/services/academic-advising-logs/index', [ - 'logs' => $this->service->paginated(...$request->validatedWithDefaults()), + 'logs' => $this->service->paginated( + ...$request->validatedWithDefaults(), + lecturerId: $request->validated('lecturer_id'), + ), 'students' => $this->studentService->getAllForSelect(), 'lecturers' => $this->lecturerService->getAllForSelect(), + 'filters' => $request->only(['lecturer_id']), ]); } diff --git a/app/Http/Controllers/Admin/Services/LetterRequestController.php b/app/Http/Controllers/Admin/Services/LetterRequestController.php index 60cb003..37eafbb 100644 --- a/app/Http/Controllers/Admin/Services/LetterRequestController.php +++ b/app/Http/Controllers/Admin/Services/LetterRequestController.php @@ -22,8 +22,12 @@ public function __construct( public function index(PaginatedRequest $request): Response { return Inertia::render('admin/services/letter-requests/index', [ - 'letterRequests' => $this->service->paginated(...$request->validatedWithDefaults()), + 'letterRequests' => $this->service->paginated( + ...$request->validatedWithDefaults(), + status: $request->validated('status'), + ), 'students' => $this->studentService->getAllForSelect(), + 'filters' => $request->only(['status']), ]); } diff --git a/app/Http/Controllers/Admin/Users/AdministratorController.php b/app/Http/Controllers/Admin/Users/AdministratorController.php index 9fb7a83..7c86c75 100644 --- a/app/Http/Controllers/Admin/Users/AdministratorController.php +++ b/app/Http/Controllers/Admin/Users/AdministratorController.php @@ -21,7 +21,11 @@ public function __construct( public function index(PaginatedRequest $request): Response { return Inertia::render('admin/users/administrators/index', [ - 'administrators' => $this->service->paginated(...$request->validatedWithDefaults()), + 'administrators' => $this->service->paginated( + ...$request->validatedWithDefaults(), + gender: $request->validated('gender'), + ), + 'filters' => $request->only(['gender']), ]); } diff --git a/app/Http/Controllers/Admin/Users/LecturerController.php b/app/Http/Controllers/Admin/Users/LecturerController.php index c4ed3e1..b5db34f 100644 --- a/app/Http/Controllers/Admin/Users/LecturerController.php +++ b/app/Http/Controllers/Admin/Users/LecturerController.php @@ -22,7 +22,13 @@ public function __construct( public function index(PaginatedRequest $request): Response { return Inertia::render('admin/users/lecturers/index', [ - 'lecturers' => $this->service->paginated(...$request->validatedWithDefaults()), + 'lecturers' => $this->service->paginated( + ...$request->validatedWithDefaults(), + gender: $request->validated('gender'), + departmentId: $request->validated('department_id'), + ), + 'departments' => $this->departmentService->getAllForSelect(), + 'filters' => $request->only(['gender', 'department_id']), ]); } diff --git a/app/Http/Controllers/Admin/Users/StudentController.php b/app/Http/Controllers/Admin/Users/StudentController.php index be335eb..efc5758 100644 --- a/app/Http/Controllers/Admin/Users/StudentController.php +++ b/app/Http/Controllers/Admin/Users/StudentController.php @@ -2,6 +2,7 @@ namespace App\Http\Controllers\Admin\Users; +use App\Enums\StudentStatus; use App\Http\Controllers\Controller; use App\Http\Requests\Admin\Users\StudentRequest; use App\Http\Requests\PaginatedRequest; @@ -24,7 +25,15 @@ public function __construct( public function index(PaginatedRequest $request): Response { return Inertia::render('admin/users/students/index', [ - 'students' => $this->service->paginated(...$request->validatedWithDefaults()), + 'students' => $this->service->paginated( + ...$request->validatedWithDefaults(), + gender: $request->validated('gender'), + departmentId: $request->validated('department_id'), + status: $request->validated('status'), + ), + 'departments' => $this->departmentService->getAllForSelect(), + 'statuses' => StudentStatus::options(), + 'filters' => $request->only(['gender', 'department_id', 'status']), ]); } diff --git a/app/Http/Requests/PaginatedRequest.php b/app/Http/Requests/PaginatedRequest.php index 73fb021..58b9852 100644 --- a/app/Http/Requests/PaginatedRequest.php +++ b/app/Http/Requests/PaginatedRequest.php @@ -2,7 +2,11 @@ namespace App\Http\Requests; +use App\Enums\ClassMethod; +use App\Enums\Gender; +use App\Enums\Semester; use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Validation\Rule; class PaginatedRequest extends FormRequest { @@ -19,6 +23,16 @@ public function rules(): array 'sort' => ['nullable', 'string'], 'direction' => ['nullable', 'string', 'in:asc,desc'], 'highlight' => ['nullable', 'integer'], + 'gender' => ['nullable', 'string', Rule::in(Gender::values())], + 'department_id' => ['nullable', 'integer'], + 'status' => ['nullable', 'string'], + 'semester' => ['nullable', 'string', Rule::in(Semester::values())], + 'is_active' => ['nullable', Rule::in(['true', 'false'])], + 'academic_term_id' => ['nullable', 'integer'], + 'method' => ['nullable', 'string', Rule::in(ClassMethod::values())], + 'course_class_id' => ['nullable', 'integer'], + 'lecturer_id' => ['nullable', 'integer'], + 'type' => ['nullable', 'string'], ]; } diff --git a/app/Services/Admin/AcademicClasses/AssignmentService.php b/app/Services/Admin/AcademicClasses/AssignmentService.php index c9e420e..146eed2 100644 --- a/app/Services/Admin/AcademicClasses/AssignmentService.php +++ b/app/Services/Admin/AcademicClasses/AssignmentService.php @@ -8,13 +8,14 @@ class AssignmentService { - public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc'): LengthAwarePaginator + public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', ?int $courseClassId = null): LengthAwarePaginator { return Assignment::query() ->select(['id', 'course_class_id', 'title', 'description', 'deadline']) ->withCount('submissions') ->with('courseClass.course:id,code,name') ->when($search, fn ($q) => $q->where('title', 'like', "%{$search}%")) + ->when($courseClassId, fn ($q) => $q->where('course_class_id', $courseClassId)) ->orderBy($sort, $direction) ->paginate($perPage); } diff --git a/app/Services/Admin/AcademicClasses/CourseRegistrationService.php b/app/Services/Admin/AcademicClasses/CourseRegistrationService.php index 7be1633..f4ebe07 100644 --- a/app/Services/Admin/AcademicClasses/CourseRegistrationService.php +++ b/app/Services/Admin/AcademicClasses/CourseRegistrationService.php @@ -9,7 +9,7 @@ class CourseRegistrationService { - public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc'): LengthAwarePaginator + public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', ?string $status = null, ?int $academicTermId = null): LengthAwarePaginator { return CourseRegistration::query() ->select(['id', 'student_id', 'academic_term_id', 'course_class_id', 'status', 'approved_by', 'created_at']) @@ -22,6 +22,8 @@ public function paginated(int $perPage = 25, string $search = '', string $sort = ]) ->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}%")))) + ->when($status, fn ($q) => $q->where('status', $status)) + ->when($academicTermId, fn ($q) => $q->where('academic_term_id', $academicTermId)) ->orderBy($sort, $direction) ->paginate($perPage); } diff --git a/app/Services/Admin/AcademicClasses/MaterialService.php b/app/Services/Admin/AcademicClasses/MaterialService.php index c1a8c59..e9eea42 100644 --- a/app/Services/Admin/AcademicClasses/MaterialService.php +++ b/app/Services/Admin/AcademicClasses/MaterialService.php @@ -8,12 +8,13 @@ class MaterialService { - public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc'): LengthAwarePaginator + public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', ?int $courseClassId = null): LengthAwarePaginator { return Material::query() ->select(['id', 'course_class_id', 'title', 'description', 'meeting_number']) ->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)) ->orderBy($sort, $direction) ->paginate($perPage); } diff --git a/app/Services/Admin/Announcements/AnnouncementService.php b/app/Services/Admin/Announcements/AnnouncementService.php index 2227b08..a16a7a5 100644 --- a/app/Services/Admin/Announcements/AnnouncementService.php +++ b/app/Services/Admin/Announcements/AnnouncementService.php @@ -7,12 +7,13 @@ class AnnouncementService { - public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc'): LengthAwarePaginator + public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', ?int $departmentId = null): LengthAwarePaginator { return Announcement::query() ->select(['id', 'title', 'content', 'department_id', 'enrollment_year', 'created_by', 'created_at']) ->with(['department:id,name', 'creator.profile']) ->when($search, fn ($q) => $q->where('title', 'like', "%{$search}%")) + ->when($departmentId, fn ($q) => $q->where('department_id', $departmentId)) ->orderBy($sort, $direction) ->paginate($perPage); } diff --git a/app/Services/Admin/FeedbackService.php b/app/Services/Admin/FeedbackService.php index 6cfea2e..1bf2942 100644 --- a/app/Services/Admin/FeedbackService.php +++ b/app/Services/Admin/FeedbackService.php @@ -40,11 +40,13 @@ public function __construct() $this->sanitizer = new HtmlSanitizer($config); } - public function paginated(User $user, int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc'): LengthAwarePaginator + public function paginated(User $user, int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', ?string $type = null, ?string $status = null): LengthAwarePaginator { return Feedback::query() ->where('user_id', $user->id) ->when($search, fn ($q) => $q->where('subject', 'like', "%{$search}%")) + ->when($type, fn ($q) => $q->where('type', $type)) + ->when($status, fn ($q) => $q->where('status', $status)) ->orderBy($sort, $direction) ->paginate($perPage); } diff --git a/app/Services/Admin/Finances/TuitionInvoiceService.php b/app/Services/Admin/Finances/TuitionInvoiceService.php index 60318c9..aabfd3e 100644 --- a/app/Services/Admin/Finances/TuitionInvoiceService.php +++ b/app/Services/Admin/Finances/TuitionInvoiceService.php @@ -7,7 +7,7 @@ class TuitionInvoiceService { - public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc'): LengthAwarePaginator + public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', ?int $academicTermId = null): LengthAwarePaginator { return TuitionInvoice::query() ->select(['id', 'student_id', 'academic_term_id', 'amount_due', 'due_date']) @@ -16,6 +16,7 @@ public function paginated(int $perPage = 25, string $search = '', string $sort = ->with(['student.user.profile', 'student.department', 'academicTerm:id,name,semester,start_date,end_date']) ->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}%")))) + ->when($academicTermId, fn ($q) => $q->where('academic_term_id', $academicTermId)) ->orderBy($sort, $direction) ->paginate($perPage); } diff --git a/app/Services/Admin/Manage/CourseClassService.php b/app/Services/Admin/Manage/CourseClassService.php index 4b99158..90b66b0 100644 --- a/app/Services/Admin/Manage/CourseClassService.php +++ b/app/Services/Admin/Manage/CourseClassService.php @@ -15,7 +15,7 @@ public function getAllForSelect(): Collection ->get(); } - public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc'): LengthAwarePaginator + public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', ?int $academicTermId = null, ?string $method = null): LengthAwarePaginator { return CourseClass::query() ->select(['id', 'course_id', 'lecturer_id', 'academic_term_id', 'class_name', 'method']) @@ -23,6 +23,8 @@ public function paginated(int $perPage = 25, string $search = '', string $sort = ->with(['course:id,code,name,department_id', 'lecturer.user.profile', 'academicTerm:id,name,semester,start_date,end_date']) ->when($search, fn ($q) => $q->where('class_name', 'like', "%{$search}%") ->orWhereHas('course', fn ($q) => $q->where('name', 'like', "%{$search}%")->orWhere('code', 'like', "%{$search}%"))) + ->when($academicTermId, fn ($q) => $q->where('academic_term_id', $academicTermId)) + ->when($method, fn ($q) => $q->where('method', $method)) ->orderBy($sort, $direction) ->paginate($perPage); } diff --git a/app/Services/Admin/Manage/CourseService.php b/app/Services/Admin/Manage/CourseService.php index ceca520..1fb477d 100644 --- a/app/Services/Admin/Manage/CourseService.php +++ b/app/Services/Admin/Manage/CourseService.php @@ -13,12 +13,13 @@ public function getAllForSelect(): Collection return Course::select(['id', 'code', 'name', 'department_id'])->get(); } - public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc'): LengthAwarePaginator + public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', ?int $departmentId = null): LengthAwarePaginator { return Course::query() ->select(['id', 'code', 'name', 'credits', 'department_id', 'semester_number']) ->with('department:id,name') ->when($search, fn ($q) => $q->where('name', 'like', "%{$search}%")->orWhere('code', 'like', "%{$search}%")) + ->when($departmentId, fn ($q) => $q->where('department_id', $departmentId)) ->orderBy($sort, $direction) ->paginate($perPage); } diff --git a/app/Services/Admin/Master/AcademicTermService.php b/app/Services/Admin/Master/AcademicTermService.php index 6e9e911..3553ce2 100644 --- a/app/Services/Admin/Master/AcademicTermService.php +++ b/app/Services/Admin/Master/AcademicTermService.php @@ -13,11 +13,13 @@ public function getAllForSelect(): Collection return AcademicTerm::select(['id', 'name', 'semester', 'start_date', 'end_date', 'is_active'])->latest()->get(); } - public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc'): LengthAwarePaginator + public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', ?string $semester = null, ?bool $isActive = null): LengthAwarePaginator { return AcademicTerm::query() ->select(['id', 'name', 'semester', 'start_date', 'end_date', 'is_active']) ->when($search, fn ($q) => $q->where('name', 'like', "%{$search}%")) + ->when($semester, fn ($q) => $q->where('semester', $semester)) + ->when($isActive !== null, fn ($q) => $q->where('is_active', $isActive)) ->orderBy($sort, $direction) ->paginate($perPage); } diff --git a/app/Services/Admin/Services/AcademicAdvisingLogService.php b/app/Services/Admin/Services/AcademicAdvisingLogService.php index 5cd2fc2..ea677b9 100644 --- a/app/Services/Admin/Services/AcademicAdvisingLogService.php +++ b/app/Services/Admin/Services/AcademicAdvisingLogService.php @@ -7,7 +7,7 @@ class AcademicAdvisingLogService { - public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc'): LengthAwarePaginator + public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', ?int $lecturerId = null): LengthAwarePaginator { return AcademicAdvisingLog::query() ->select(['id', 'student_id', 'lecturer_id', 'topic', 'notes', 'session_date', 'created_at']) @@ -19,6 +19,7 @@ public function paginated(int $perPage = 25, string $search = '', string $sort = ->when($search, fn ($q) => $q->where('topic', 'like', "%{$search}%") ->orWhereHas('student', fn ($q) => $q->where('student_number', 'like', "%{$search}%") ->orWhereHas('user.profile', fn ($q) => $q->where('full_name', 'like', "%{$search}%")))) + ->when($lecturerId, fn ($q) => $q->where('lecturer_id', $lecturerId)) ->orderBy($sort, $direction) ->paginate($perPage); } diff --git a/app/Services/Admin/Services/LetterRequestService.php b/app/Services/Admin/Services/LetterRequestService.php index efa1107..f40b37d 100644 --- a/app/Services/Admin/Services/LetterRequestService.php +++ b/app/Services/Admin/Services/LetterRequestService.php @@ -9,7 +9,7 @@ class LetterRequestService { - public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc'): LengthAwarePaginator + public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', ?string $status = null): LengthAwarePaginator { return LetterRequest::query() ->select(['id', 'student_id', 'letter_type', 'purpose', 'status', 'processed_by', 'submitted_at', 'completed_at']) @@ -17,6 +17,7 @@ public function paginated(int $perPage = 25, string $search = '', string $sort = ->when($search, fn ($q) => $q->where('letter_type', 'like', "%{$search}%") ->orWhereHas('student', fn ($q) => $q->where('student_number', 'like', "%{$search}%") ->orWhereHas('user.profile', fn ($q) => $q->where('full_name', 'like', "%{$search}%")))) + ->when($status, fn ($q) => $q->where('status', $status)) ->orderBy($sort, $direction) ->paginate($perPage); } diff --git a/app/Services/Admin/Users/AdministratorService.php b/app/Services/Admin/Users/AdministratorService.php index 6e3520f..f8f31c6 100644 --- a/app/Services/Admin/Users/AdministratorService.php +++ b/app/Services/Admin/Users/AdministratorService.php @@ -9,7 +9,7 @@ class AdministratorService { - public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc'): LengthAwarePaginator + public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', ?string $gender = null): LengthAwarePaginator { return User::with(['profile', 'roles']) ->whereHas('roles', fn ($q) => $q->whereIn('name', ['staff-admin', 'staff-keuangan'])) @@ -18,6 +18,7 @@ public function paginated(int $perPage = 25, string $search = '', string $sort = ->orWhere('email', 'like', "%{$search}%") ->orWhereHas('profile', fn ($q) => $q->where('full_name', 'like', "%{$search}%")); })) + ->when($gender, fn ($q) => $q->whereHas('profile', fn ($q) => $q->where('gender', $gender))) ->orderBy($sort, $direction) ->paginate($perPage); } diff --git a/app/Services/Admin/Users/LecturerService.php b/app/Services/Admin/Users/LecturerService.php index 9fceeb0..fa2fb9d 100644 --- a/app/Services/Admin/Users/LecturerService.php +++ b/app/Services/Admin/Users/LecturerService.php @@ -16,7 +16,7 @@ public function getAllForSelect(): Collection return Lecturer::with('user.profile')->get(); } - public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc'): LengthAwarePaginator + public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', ?string $gender = null, ?int $departmentId = null): LengthAwarePaginator { return User::with(['profile', 'lecturer.department']) ->whereHas('roles', fn ($q) => $q->where('name', 'dosen')) @@ -26,6 +26,8 @@ public function paginated(int $perPage = 25, string $search = '', string $sort = ->orWhereHas('profile', fn ($q) => $q->where('full_name', 'like', "%{$search}%")) ->orWhereHas('lecturer', fn ($q) => $q->where('lecturer_number', 'like', "%{$search}%")); })) + ->when($gender, fn ($q) => $q->whereHas('profile', fn ($q) => $q->where('gender', $gender))) + ->when($departmentId, fn ($q) => $q->whereHas('lecturer', fn ($q) => $q->where('department_id', $departmentId))) ->orderBy($sort, $direction) ->paginate($perPage); } diff --git a/app/Services/Admin/Users/StudentService.php b/app/Services/Admin/Users/StudentService.php index f3861d8..3169d95 100644 --- a/app/Services/Admin/Users/StudentService.php +++ b/app/Services/Admin/Users/StudentService.php @@ -16,7 +16,7 @@ public function getAllForSelect(): Collection return Student::with(['user.profile', 'department'])->get(); } - public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc'): LengthAwarePaginator + public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', ?string $gender = null, ?int $departmentId = null, ?string $status = null): LengthAwarePaginator { return User::with(['profile', 'student.department']) ->whereHas('roles', fn ($q) => $q->where('name', 'mahasiswa')) @@ -26,6 +26,9 @@ public function paginated(int $perPage = 25, string $search = '', string $sort = ->orWhereHas('profile', fn ($q) => $q->where('full_name', 'like', "%{$search}%")) ->orWhereHas('student', fn ($q) => $q->where('student_number', 'like', "%{$search}%")); })) + ->when($gender, fn ($q) => $q->whereHas('profile', fn ($q) => $q->where('gender', $gender))) + ->when($departmentId, fn ($q) => $q->whereHas('student', fn ($q) => $q->where('department_id', $departmentId))) + ->when($status, fn ($q) => $q->whereHas('student', fn ($q) => $q->where('status', $status))) ->orderBy($sort, $direction) ->paginate($perPage); } diff --git a/docs/code-style.md b/docs/code-style.md index 26378c0..c3ad710 100644 --- a/docs/code-style.md +++ b/docs/code-style.md @@ -205,7 +205,88 @@ ## 5. Service ✅ --- -## 6. Wayfinder ✅ +## 6. Filter dropdown di halaman listing ✅ + +Tombol filter (ikon corong di samping search box) memakai dialog, bukan +popover — komponennya `resources/js/components/filter-dialog.tsx` +(`` +dilewatkan lewat prop `toolbar` milik ``, otomatis nongol di +sebelah kanan search box). + +Sudah diterapkan di semua halaman listing yang punya field layak difilter: + +| Halaman | Field filter | +| --------------------------- | -------------------------------- | +| Administrator/Dosen/Mahasiswa | Jenis Kelamin (+ Jurusan, Status untuk Dosen/Mahasiswa) | +| Periode Akademik | Semester, Status Aktif | +| Registrasi KRS | Status, Periode Akademik | +| Materi, Tugas | Kelas | +| Mata Kuliah | Jurusan | +| Kelas Mata Kuliah | Periode Akademik, Metode | +| Pengumuman | Jurusan | +| Tagihan | Periode Akademik | +| Surat Permohonan | Status | +| Bimbingan Akademik | Dosen | +| Kritik dan Saran | Jenis, Status | + +Halaman tanpa field yang layak difilter (mis. Jurusan/Master — tabelnya kecil, +tidak butuh filter) sengaja tidak diberi `FilterDialog`. + +**Perilaku UI (jangan diubah tanpa alasan kuat):** + +- **Langsung diterapkan** — begitu satu field di dalam dialog dipilih + (`onValueChange`), filter langsung jalan (navigasi Inertia), tidak ada + tombol "Terapkan" terpisah yang harus diklik dulu. +- **Field filter disusun 2 kolom per baris** (`grid grid-cols-2 gap-4`) kalau + field-nya lebih dari satu; kalau cuma 1 field, 1 kolom saja (`grid gap-4`) + — jangan sisakan slot kosong di grid. +- **Tombol "Hapus Filter" ada DI LUAR dialog**, sejajar di samping tombol + ikon filter (bukan di footer dialog), berupa tombol teks (`variant="ghost"` + + label "Hapus Filter"), dan cuma muncul kalau ada filter yang aktif. + +**Backend:** + +1. Field filter (mis. `gender`, `department_id`, `status`) didaftarkan di + `App\Http\Requests\PaginatedRequest::rules()` sebagai `nullable` — request + ini dipakai bersama oleh semua halaman listing, jadi field filter yang + sifatnya umum (dipakai lebih dari satu fitur) taruh di sini alih-alih + bikin FormRequest baru per halaman. +2. `Service::paginated()` menerima parameter filter tambahan sebagai + parameter bernama opsional di akhir signature (setelah + `$perPage/$search/$sort/$direction`), mis. + `paginated(..., ?string $gender = null, ?int $departmentId = null)`, lalu + diterapkan dengan `->when($gender, fn ($q) => ...)`. +3. Controller memanggilnya secara **eksplisit per parameter** — jangan + nge-spread seluruh `$request->validated()` mentah-mentah ke + `paginated()`, karena tidak semua Service menerima semua field filter + (bisa error "Unknown named parameter"): + + ```php + 'students' => $this->service->paginated( + ...$request->validatedWithDefaults(), + gender: $request->validated('gender'), + departmentId: $request->validated('department_id'), + status: $request->validated('status'), + ), + 'filters' => $request->only(['gender', 'department_id', 'status']), + ``` + +**Frontend:** + +1. `Props.filters` menampung nilai filter yang sedang aktif (dari query + string, dikirim controller lewat `$request->only([...])`). +2. `useServerTable({..., filters})` — prop `filters` diteruskan apa adanya + dari `Props.filters` (bukan `useState` terpisah, karena Inertia sudah + selalu mengirim prop terbaru setiap navigasi). +3. Definisikan `filterFields: FilterField[]` (key, label, options) sesuai + data yang tersedia di halaman itu (mis. `departments` dari prop untuk + Select jurusan), lalu render + `` + sebagai `toolbar` di ``. + +--- + +## 7. Wayfinder ✅ Selalu jalankan generate dengan flag form variant, supaya `
` tidak error saat runtime: diff --git a/resources/js/components/filter-dialog.tsx b/resources/js/components/filter-dialog.tsx new file mode 100644 index 0000000..adf88c4 --- /dev/null +++ b/resources/js/components/filter-dialog.tsx @@ -0,0 +1,136 @@ +import { Filter, X } from 'lucide-react'; +import { useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog'; +import { Label } from '@/components/ui/label'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; + +export type FilterOption = { + value: string; + label: string; +}; + +export type FilterField = { + key: string; + label: string; + placeholder?: string; + options: FilterOption[]; +}; + +type FilterDialogProps = { + fields: FilterField[]; + activeFilters: Record; + onApply: (filters: Record) => void; +}; + +export function FilterDialog({ + fields, + activeFilters, + onApply, +}: FilterDialogProps) { + const [open, setOpen] = useState(false); + + const activeCount = fields.filter( + (field) => activeFilters[field.key], + ).length; + + function handleChange(key: string, value: string) { + const next: Record = {}; + + fields.forEach((field) => { + const current = + field.key === key ? value : activeFilters[field.key]; + + if (current && current !== 'all') { + next[field.key] = current; + } + }); + + onApply(next); + } + + function handleClear() { + onApply({}); + } + + return ( +
+ + + + + + + Filter + +
1 + ? 'grid grid-cols-2 gap-4 py-4' + : 'grid gap-4 py-4' + } + > + {fields.map((field) => ( +
+ + +
+ ))} +
+
+
+ + {activeCount > 0 && ( + + )} +
+ ); +} diff --git a/resources/js/hooks/use-server-table.tsx b/resources/js/hooks/use-server-table.tsx index 8b50624..fa7cafb 100644 --- a/resources/js/hooks/use-server-table.tsx +++ b/resources/js/hooks/use-server-table.tsx @@ -1,6 +1,6 @@ -import type { PaginationState } from '@/components/data-table'; import { router } from '@inertiajs/react'; import { useCallback, useState } from 'react'; +import type { PaginationState } from '@/components/data-table'; type UseServerTableOptions = { route: () => string; @@ -80,11 +80,26 @@ export function useServerTable({ route(), filterWithParams ? { - ...newFilters, - page: 1, - per_page: pagination.per_page, - search, - } + ...newFilters, + page: 1, + per_page: pagination.per_page, + search, + } + : newFilters, + { preserveState: true, replace: true }, + ); + } + + function applyFilters(newFilters: Record) { + router.get( + route(), + filterWithParams + ? { + ...newFilters, + page: 1, + per_page: pagination.per_page, + search, + } : newFilters, { preserveState: true, replace: true }, ); @@ -95,10 +110,10 @@ export function useServerTable({ route(), filterWithParams ? { - page: 1, - per_page: pagination.per_page, - search, - } + page: 1, + per_page: pagination.per_page, + search, + } : {}, { preserveState: true, replace: true }, ); @@ -113,6 +128,7 @@ export function useServerTable({ handlePerPageChange, handleSearchChange, applyFilter, + applyFilters, clearFilters, }; } diff --git a/resources/js/pages/admin/academic-classes/assignments/index.tsx b/resources/js/pages/admin/academic-classes/assignments/index.tsx index a5d016b..e73655f 100644 --- a/resources/js/pages/admin/academic-classes/assignments/index.tsx +++ b/resources/js/pages/admin/academic-classes/assignments/index.tsx @@ -6,6 +6,8 @@ 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 { FilterDialog } from '@/components/filter-dialog'; import { FormDialog } from '@/components/form-dialog'; import InputError from '@/components/input-error'; import { PageHeader } from '@/components/page-header'; @@ -46,6 +48,9 @@ type Props = { }; courseClasses: CourseClassOption[]; highlight?: number; + filters: { + course_class_id?: string; + }; }; function courseClassLabel(courseClass: CourseClassOption): string { @@ -60,11 +65,23 @@ export default function AssignmentIndex({ assignments, courseClasses, highlight, + filters, }: Props) { const [createOpen, setCreateOpen] = useState(false); const [editing, setEditing] = useState(null); const [deleting, setDeleting] = useState(null); + const filterFields: FilterField[] = [ + { + key: 'course_class_id', + label: 'Kelas', + options: courseClasses.map((courseClass) => ({ + value: String(courseClass.id), + label: courseClassLabel(courseClass), + })), + }, + ]; + const pagination: PaginationState = { current_page: assignments.current_page, last_page: assignments.last_page, @@ -77,9 +94,11 @@ export default function AssignmentIndex({ handlePageChange, handlePerPageChange, handleSearchChange, + applyFilters, } = useServerTable({ route: () => assignmentIndex.url(), pagination, + filters, }); function handleDelete() { @@ -168,6 +187,13 @@ export default function AssignmentIndex({ onPerPageChange={handlePerPageChange} onSearchChange={handleSearchChange} searchValue={search} + toolbar={ + + } /> (null); const [deleting, setDeleting] = useState(null); + const filterFields: FilterField[] = [ + { + key: 'status', + label: 'Status', + options: RegistrationStatuses.map((status) => ({ + value: status, + label: RegistrationStatusLabels[status], + })), + }, + { + key: 'academic_term_id', + label: 'Periode Akademik', + options: academicTerms.map((term) => ({ + value: String(term.id), + label: term.name, + })), + }, + ]; + const pagination: PaginationState = { current_page: registrations.current_page, last_page: registrations.last_page, @@ -95,9 +121,11 @@ export default function CourseRegistrationIndex({ handlePageChange, handlePerPageChange, handleSearchChange, + applyFilters, } = useServerTable({ route: () => courseRegistrationIndex.url(), pagination, + filters, }); function handleDelete() { @@ -177,6 +205,13 @@ export default function CourseRegistrationIndex({ searchValue={search} searchKey="student" searchPlaceholder="Cari nama atau NIM mahasiswa..." + toolbar={ + + } /> (null); const [deleting, setDeleting] = useState(null); + const filterFields: FilterField[] = [ + { + key: 'course_class_id', + label: 'Kelas', + options: courseClasses.map((courseClass) => ({ + value: String(courseClass.id), + label: courseClassLabel(courseClass), + })), + }, + ]; + const pagination: PaginationState = { current_page: materials.current_page, last_page: materials.last_page, @@ -76,9 +93,11 @@ export default function MaterialIndex({ handlePageChange, handlePerPageChange, handleSearchChange, + applyFilters, } = useServerTable({ route: () => materialIndex.url(), pagination, + filters, }); function handleDelete() { @@ -167,6 +186,13 @@ export default function MaterialIndex({ onPerPageChange={handlePerPageChange} onSearchChange={handleSearchChange} searchValue={search} + toolbar={ + + } /> (null); const [deleting, setDeleting] = useState(null); + const filterFields: FilterField[] = [ + { + key: 'department_id', + label: 'Jurusan', + options: departments.map((department) => ({ + value: String(department.id), + label: department.name, + })), + }, + ]; + const pagination: PaginationState = { current_page: announcements.current_page, last_page: announcements.last_page, @@ -64,9 +81,11 @@ export default function AnnouncementIndex({ handlePageChange, handlePerPageChange, handleSearchChange, + applyFilters, } = useServerTable({ route: () => announcementIndex.url(), pagination, + filters, }); function handleDelete() { @@ -140,6 +159,13 @@ export default function AnnouncementIndex({ searchValue={search} searchKey="title" searchPlaceholder="Cari judul pengumuman..." + toolbar={ + + } /> (null); const [deleting, setDeleting] = useState(null); + const filterFields: FilterField[] = [ + { + key: 'type', + label: 'Jenis', + options: types, + }, + { + key: 'status', + label: 'Status', + options: statuses, + }, + ]; + const pagination: PaginationState = { current_page: feedbacks.current_page, last_page: feedbacks.last_page, @@ -58,9 +83,11 @@ export default function FeedbackIndex({ feedbacks, types }: Props) { handlePageChange, handlePerPageChange, handleSearchChange, + applyFilters, } = useServerTable({ route: () => feedbackIndex.url(), pagination, + filters, }); function handleDelete() { @@ -133,6 +160,13 @@ export default function FeedbackIndex({ feedbacks, types }: Props) { searchValue={search} searchKey="subject" searchPlaceholder="Cari subjek..." + toolbar={ + + } /> (null); const [deleting, setDeleting] = useState(null); + const filterFields: FilterField[] = [ + { + key: 'academic_term_id', + label: 'Periode Akademik', + options: academicTerms.map((term) => ({ + value: String(term.id), + label: term.name, + })), + }, + ]; + const pagination: PaginationState = { current_page: invoices.current_page, last_page: invoices.last_page, @@ -73,9 +90,11 @@ export default function TuitionInvoiceIndex({ handlePageChange, handlePerPageChange, handleSearchChange, + applyFilters, } = useServerTable({ route: () => tuitionInvoiceIndex.url(), pagination, + filters, }); function handleDelete() { @@ -151,6 +170,13 @@ export default function TuitionInvoiceIndex({ searchValue={search} searchKey="student" searchPlaceholder="Cari nama atau NIM mahasiswa..." + toolbar={ + + } /> (null); const [deleting, setDeleting] = useState(null); + const filterFields: FilterField[] = [ + { + key: 'academic_term_id', + label: 'Periode Akademik', + options: academicTerms.map((term) => ({ + value: String(term.id), + label: term.name, + })), + }, + { + key: 'method', + label: 'Metode', + options: ClassMethods.map((method) => ({ + value: method, + label: ClassMethodLabels[method], + })), + }, + ]; + const pagination: PaginationState = { current_page: courseClasses.current_page, last_page: courseClasses.last_page, @@ -74,9 +100,11 @@ export default function CourseClassIndex({ handlePageChange, handlePerPageChange, handleSearchChange, + applyFilters, } = useServerTable({ route: () => courseClassIndex.url(), pagination, + filters, }); function handleDelete() { @@ -169,6 +197,13 @@ export default function CourseClassIndex({ onPerPageChange={handlePerPageChange} onSearchChange={handleSearchChange} searchValue={search} + toolbar={ + + } /> (null); const [deleting, setDeleting] = useState(null); + const filterFields: FilterField[] = [ + { + key: 'department_id', + label: 'Jurusan', + options: departments.map((department) => ({ + value: String(department.id), + label: department.name, + })), + }, + ]; + const pagination: PaginationState = { current_page: courses.current_page, last_page: courses.last_page, @@ -62,9 +79,11 @@ export default function CourseIndex({ handlePageChange, handlePerPageChange, handleSearchChange, + applyFilters, } = useServerTable({ route: () => courseIndex.url(), pagination, + filters, }); function handleDelete() { @@ -153,6 +172,13 @@ export default function CourseIndex({ onPerPageChange={handlePerPageChange} onSearchChange={handleSearchChange} searchValue={search} + toolbar={ + + } /> ({ + value, + label: SemesterLabels[value], + })), + }, + { + key: 'is_active', + label: 'Status', + options: [ + { value: 'true', label: 'Aktif' }, + { value: 'false', label: 'Tidak Aktif' }, + ], + }, +]; + +export default function AcademicTermIndex({ + academicTerms, + highlight, + filters, +}: Props) { const [createOpen, setCreateOpen] = useState(false); const [editing, setEditing] = useState(null); const [deleting, setDeleting] = useState(null); @@ -52,9 +81,11 @@ export default function AcademicTermIndex({ academicTerms, highlight }: Props) { handlePageChange, handlePerPageChange, handleSearchChange, + applyFilters, } = useServerTable({ route: () => academicTermIndex.url(), pagination, + filters, }); function handleDelete() { @@ -114,10 +145,7 @@ export default function AcademicTermIndex({ academicTerms, highlight }: Props) { } /> - + + } /> {Object.values(Semester).map((value) => ( -
- -