Merge pull request 'feat: add filter functionality to various admin pages' (#45) from feat/add-filter-funcionality into dev
Reviewed-on: #45
This commit is contained in:
commit
5c0cadcebf
@ -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']),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -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']),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -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']),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -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']),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -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']),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -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']),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -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']),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -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']),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -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']),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -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']),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -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']),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -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']),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -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']),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -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']),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -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'],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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`
|
||||
(`<FilterDialog fields={...} activeFilters={filters} onApply={applyFilters} />`
|
||||
dilewatkan lewat prop `toolbar` milik `<DataTable>`, 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
|
||||
`<FilterDialog fields={filterFields} activeFilters={filters} onApply={applyFilters} />`
|
||||
sebagai `toolbar` di `<DataTable>`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Wayfinder ✅
|
||||
|
||||
Selalu jalankan generate dengan flag form variant, supaya `<Form
|
||||
{...Controller.method.form()}>` tidak error saat runtime:
|
||||
|
||||
136
resources/js/components/filter-dialog.tsx
Normal file
136
resources/js/components/filter-dialog.tsx
Normal file
@ -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<string, string | undefined>;
|
||||
onApply: (filters: Record<string, string>) => 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<string, string> = {};
|
||||
|
||||
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 (
|
||||
<div className="flex items-center gap-2">
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="icon" className="relative">
|
||||
<Filter className="h-4 w-4" />
|
||||
{activeCount > 0 && (
|
||||
<span className="absolute -top-1.5 -right-1.5 flex h-4 w-4 items-center justify-center rounded-full bg-primary text-[10px] text-primary-foreground">
|
||||
{activeCount}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Filter</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div
|
||||
className={
|
||||
fields.length > 1
|
||||
? 'grid grid-cols-2 gap-4 py-4'
|
||||
: 'grid gap-4 py-4'
|
||||
}
|
||||
>
|
||||
{fields.map((field) => (
|
||||
<div className="grid gap-2" key={field.key}>
|
||||
<Label>{field.label}</Label>
|
||||
<Select
|
||||
value={activeFilters[field.key] ?? 'all'}
|
||||
onValueChange={(value) =>
|
||||
handleChange(field.key, value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue
|
||||
placeholder={
|
||||
field.placeholder ?? 'Semua'
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">
|
||||
Semua
|
||||
</SelectItem>
|
||||
{field.options.map((option) => (
|
||||
<SelectItem
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{activeCount > 0 && (
|
||||
<Button variant="ghost" size="sm" onClick={handleClear}>
|
||||
<X className="h-4 w-4" />
|
||||
Hapus Filter
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -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<string, string | undefined>) {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
@ -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<Assignment | null>(null);
|
||||
const [deleting, setDeleting] = useState<Assignment | null>(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={
|
||||
<FilterDialog
|
||||
fields={filterFields}
|
||||
activeFilters={filters}
|
||||
onApply={applyFilters}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<DeleteConfirmDialog
|
||||
|
||||
@ -4,6 +4,8 @@ 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 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';
|
||||
@ -54,6 +56,10 @@ type Props = {
|
||||
courseClasses: CourseRegistrationCourseClass[];
|
||||
lecturers: LecturerOption[];
|
||||
highlight?: number;
|
||||
filters: {
|
||||
status?: string;
|
||||
academic_term_id?: string;
|
||||
};
|
||||
};
|
||||
|
||||
function studentLabel(student: CourseRegistrationStudent): string {
|
||||
@ -78,11 +84,31 @@ export default function CourseRegistrationIndex({
|
||||
courseClasses,
|
||||
lecturers,
|
||||
highlight,
|
||||
filters,
|
||||
}: Props) {
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [editing, setEditing] = useState<CourseRegistration | null>(null);
|
||||
const [deleting, setDeleting] = useState<CourseRegistration | null>(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={
|
||||
<FilterDialog
|
||||
fields={filterFields}
|
||||
activeFilters={filters}
|
||||
onApply={applyFilters}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<DeleteConfirmDialog
|
||||
|
||||
@ -5,6 +5,8 @@ import type { PaginationState } from '@/components/data-table';
|
||||
import { DataTable } from '@/components/data-table';
|
||||
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';
|
||||
@ -45,6 +47,9 @@ type Props = {
|
||||
};
|
||||
courseClasses: CourseClassOption[];
|
||||
highlight?: number;
|
||||
filters: {
|
||||
course_class_id?: string;
|
||||
};
|
||||
};
|
||||
|
||||
function courseClassLabel(courseClass: CourseClassOption): string {
|
||||
@ -59,11 +64,23 @@ export default function MaterialIndex({
|
||||
materials,
|
||||
courseClasses,
|
||||
highlight,
|
||||
filters,
|
||||
}: Props) {
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [editing, setEditing] = useState<Material | null>(null);
|
||||
const [deleting, setDeleting] = useState<Material | null>(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={
|
||||
<FilterDialog
|
||||
fields={filterFields}
|
||||
activeFilters={filters}
|
||||
onApply={applyFilters}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<DeleteConfirmDialog
|
||||
|
||||
@ -4,6 +4,8 @@ import { useRef, useState } from 'react';
|
||||
import type { PaginationState } from '@/components/data-table';
|
||||
import { DataTable } from '@/components/data-table';
|
||||
import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog';
|
||||
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';
|
||||
@ -41,17 +43,32 @@ type Props = {
|
||||
};
|
||||
departments: AnnouncementDepartment[];
|
||||
highlight?: number;
|
||||
filters: {
|
||||
department_id?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export default function AnnouncementIndex({
|
||||
announcements,
|
||||
departments,
|
||||
highlight,
|
||||
filters,
|
||||
}: Props) {
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [editing, setEditing] = useState<Announcement | null>(null);
|
||||
const [deleting, setDeleting] = useState<Announcement | null>(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={
|
||||
<FilterDialog
|
||||
fields={filterFields}
|
||||
activeFilters={filters}
|
||||
onApply={applyFilters}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<DeleteConfirmDialog
|
||||
|
||||
@ -4,6 +4,8 @@ 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 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';
|
||||
@ -39,13 +41,36 @@ type Props = {
|
||||
total: number;
|
||||
};
|
||||
types: FeedbackTypeOption[];
|
||||
statuses: FeedbackTypeOption[];
|
||||
filters: {
|
||||
type?: string;
|
||||
status?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export default function FeedbackIndex({ feedbacks, types }: Props) {
|
||||
export default function FeedbackIndex({
|
||||
feedbacks,
|
||||
types,
|
||||
statuses,
|
||||
filters,
|
||||
}: Props) {
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [editing, setEditing] = useState<Feedback | null>(null);
|
||||
const [deleting, setDeleting] = useState<Feedback | null>(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={
|
||||
<FilterDialog
|
||||
fields={filterFields}
|
||||
activeFilters={filters}
|
||||
onApply={applyFilters}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<DeleteConfirmDialog
|
||||
|
||||
@ -6,6 +6,8 @@ import type { PaginationState } from '@/components/data-table';
|
||||
import { DataTable } from '@/components/data-table';
|
||||
import { DatePicker } from '@/components/date-picker';
|
||||
import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog';
|
||||
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';
|
||||
@ -45,6 +47,9 @@ type Props = {
|
||||
students: TuitionInvoiceStudent[];
|
||||
academicTerms: AcademicTermOption[];
|
||||
highlight?: number;
|
||||
filters: {
|
||||
academic_term_id?: string;
|
||||
};
|
||||
};
|
||||
|
||||
function studentLabel(student: TuitionInvoiceStudent): string {
|
||||
@ -56,11 +61,23 @@ export default function TuitionInvoiceIndex({
|
||||
students,
|
||||
academicTerms,
|
||||
highlight,
|
||||
filters,
|
||||
}: Props) {
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [editing, setEditing] = useState<TuitionInvoice | null>(null);
|
||||
const [deleting, setDeleting] = useState<TuitionInvoice | null>(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={
|
||||
<FilterDialog
|
||||
fields={filterFields}
|
||||
activeFilters={filters}
|
||||
onApply={applyFilters}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<DeleteConfirmDialog
|
||||
|
||||
@ -4,6 +4,8 @@ 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 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';
|
||||
@ -49,6 +51,10 @@ type Props = {
|
||||
lecturers: Lecturer[];
|
||||
academicTerms: AcademicTerm[];
|
||||
highlight?: number;
|
||||
filters: {
|
||||
academic_term_id?: string;
|
||||
method?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export default function CourseClassIndex({
|
||||
@ -57,11 +63,31 @@ export default function CourseClassIndex({
|
||||
lecturers,
|
||||
academicTerms,
|
||||
highlight,
|
||||
filters,
|
||||
}: Props) {
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [editing, setEditing] = useState<CourseClass | null>(null);
|
||||
const [deleting, setDeleting] = useState<CourseClass | null>(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={
|
||||
<FilterDialog
|
||||
fields={filterFields}
|
||||
activeFilters={filters}
|
||||
onApply={applyFilters}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<DeleteConfirmDialog
|
||||
|
||||
@ -4,6 +4,8 @@ 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 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';
|
||||
@ -39,17 +41,32 @@ type Props = {
|
||||
};
|
||||
departments: Department[];
|
||||
highlight?: number;
|
||||
filters: {
|
||||
department_id?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export default function CourseIndex({
|
||||
courses,
|
||||
departments,
|
||||
highlight,
|
||||
filters,
|
||||
}: Props) {
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [editing, setEditing] = useState<Course | null>(null);
|
||||
const [deleting, setDeleting] = useState<Course | null>(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={
|
||||
<FilterDialog
|
||||
fields={filterFields}
|
||||
activeFilters={filters}
|
||||
onApply={applyFilters}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<DeleteConfirmDialog
|
||||
|
||||
@ -5,6 +5,8 @@ import type { PaginationState } from '@/components/data-table';
|
||||
import { DataTable } from '@/components/data-table';
|
||||
import { DatePicker } from '@/components/date-picker';
|
||||
import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog';
|
||||
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';
|
||||
@ -33,9 +35,36 @@ type Props = {
|
||||
total: number;
|
||||
};
|
||||
highlight?: number;
|
||||
filters: {
|
||||
semester?: string;
|
||||
is_active?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export default function AcademicTermIndex({ academicTerms, highlight }: Props) {
|
||||
const filterFields: FilterField[] = [
|
||||
{
|
||||
key: 'semester',
|
||||
label: 'Semester',
|
||||
options: Object.values(Semester).map((value) => ({
|
||||
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<AcademicTerm | null>(null);
|
||||
const [deleting, setDeleting] = useState<AcademicTerm | null>(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) {
|
||||
}
|
||||
/>
|
||||
|
||||
<CreateForm
|
||||
open={createOpen}
|
||||
onOpenChange={setCreateOpen}
|
||||
/>
|
||||
<CreateForm open={createOpen} onOpenChange={setCreateOpen} />
|
||||
|
||||
<EditForm
|
||||
key={editing?.id}
|
||||
@ -141,6 +169,13 @@ export default function AcademicTermIndex({ academicTerms, highlight }: Props) {
|
||||
onPerPageChange={handlePerPageChange}
|
||||
onSearchChange={handleSearchChange}
|
||||
searchValue={search}
|
||||
toolbar={
|
||||
<FilterDialog
|
||||
fields={filterFields}
|
||||
activeFilters={filters}
|
||||
onApply={applyFilters}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<DeleteConfirmDialog
|
||||
@ -207,9 +242,18 @@ function CreateForm({
|
||||
className="flex flex-row gap-4"
|
||||
>
|
||||
{Object.values(Semester).map((value) => (
|
||||
<div key={value} className="flex items-center gap-2">
|
||||
<RadioGroupItem value={value} id={`create-${value}`} />
|
||||
<Label htmlFor={`create-${value}`} className="font-normal">
|
||||
<div
|
||||
key={value}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<RadioGroupItem
|
||||
value={value}
|
||||
id={`create-${value}`}
|
||||
/>
|
||||
<Label
|
||||
htmlFor={`create-${value}`}
|
||||
className="font-normal"
|
||||
>
|
||||
{SemesterLabels[value]}
|
||||
</Label>
|
||||
</div>
|
||||
@ -220,9 +264,18 @@ function CreateForm({
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label>
|
||||
Tanggal Mulai <span className="text-destructive">*</span>
|
||||
Tanggal Mulai{' '}
|
||||
<span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<input type="hidden" name="start_date" value={startDate ? startDate.toISOString().split('T')[0] : ''} />
|
||||
<input
|
||||
type="hidden"
|
||||
name="start_date"
|
||||
value={
|
||||
startDate
|
||||
? startDate.toISOString().split('T')[0]
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
<DatePicker
|
||||
value={startDate}
|
||||
onChange={setStartDate}
|
||||
@ -231,8 +284,19 @@ function CreateForm({
|
||||
<InputError message={errors.start_date} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>Tanggal Selesai <span className="text-destructive">*</span></Label>
|
||||
<input type="hidden" name="end_date" value={endDate ? endDate.toISOString().split('T')[0] : ''} />
|
||||
<Label>
|
||||
Tanggal Selesai{' '}
|
||||
<span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<input
|
||||
type="hidden"
|
||||
name="end_date"
|
||||
value={
|
||||
endDate
|
||||
? endDate.toISOString().split('T')[0]
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
<DatePicker
|
||||
value={endDate}
|
||||
onChange={setEndDate}
|
||||
@ -286,8 +350,7 @@ function EditForm({
|
||||
<div className="grid gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="edit-name">
|
||||
Nama{' '}
|
||||
<span className="text-destructive">*</span>
|
||||
Nama <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="edit-name"
|
||||
@ -299,7 +362,8 @@ function EditForm({
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>
|
||||
Semester <span className="text-destructive">*</span>
|
||||
Semester{' '}
|
||||
<span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<RadioGroup
|
||||
name="semester"
|
||||
@ -307,9 +371,18 @@ function EditForm({
|
||||
className="flex flex-row gap-4"
|
||||
>
|
||||
{Object.values(Semester).map((value) => (
|
||||
<div key={value} className="flex items-center gap-2">
|
||||
<RadioGroupItem value={value} id={`edit-${value}`} />
|
||||
<Label htmlFor={`edit-${value}`} className="font-normal">
|
||||
<div
|
||||
key={value}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<RadioGroupItem
|
||||
value={value}
|
||||
id={`edit-${value}`}
|
||||
/>
|
||||
<Label
|
||||
htmlFor={`edit-${value}`}
|
||||
className="font-normal"
|
||||
>
|
||||
{SemesterLabels[value]}
|
||||
</Label>
|
||||
</div>
|
||||
@ -319,8 +392,21 @@ function EditForm({
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label>Tanggal Mulai <span className="text-destructive">*</span></Label>
|
||||
<input type="hidden" name="start_date" value={startDate ? startDate.toISOString().split('T')[0] : ''} />
|
||||
<Label>
|
||||
Tanggal Mulai{' '}
|
||||
<span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<input
|
||||
type="hidden"
|
||||
name="start_date"
|
||||
value={
|
||||
startDate
|
||||
? startDate
|
||||
.toISOString()
|
||||
.split('T')[0]
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
<DatePicker
|
||||
value={startDate}
|
||||
onChange={setStartDate}
|
||||
@ -329,8 +415,21 @@ function EditForm({
|
||||
<InputError message={errors.start_date} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>Tanggal Selesai <span className="text-destructive">*</span></Label>
|
||||
<input type="hidden" name="end_date" value={endDate ? endDate.toISOString().split('T')[0] : ''} />
|
||||
<Label>
|
||||
Tanggal Selesai{' '}
|
||||
<span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<input
|
||||
type="hidden"
|
||||
name="end_date"
|
||||
value={
|
||||
endDate
|
||||
? endDate
|
||||
.toISOString()
|
||||
.split('T')[0]
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
<DatePicker
|
||||
value={endDate}
|
||||
onChange={setEndDate}
|
||||
|
||||
@ -5,6 +5,8 @@ import type { PaginationState } from '@/components/data-table';
|
||||
import { DataTable } from '@/components/data-table';
|
||||
import { DateTimeField } from '@/components/datetime-field';
|
||||
import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog';
|
||||
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';
|
||||
@ -44,6 +46,9 @@ type Props = {
|
||||
students: AcademicAdvisingLogStudent[];
|
||||
lecturers: AcademicAdvisingLogLecturer[];
|
||||
highlight?: number;
|
||||
filters: {
|
||||
lecturer_id?: string;
|
||||
};
|
||||
};
|
||||
|
||||
function studentLabel(student: AcademicAdvisingLogStudent): string {
|
||||
@ -59,11 +64,23 @@ export default function AcademicAdvisingLogIndex({
|
||||
students,
|
||||
lecturers,
|
||||
highlight,
|
||||
filters,
|
||||
}: Props) {
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [editing, setEditing] = useState<AcademicAdvisingLog | null>(null);
|
||||
const [deleting, setDeleting] = useState<AcademicAdvisingLog | null>(null);
|
||||
|
||||
const filterFields: FilterField[] = [
|
||||
{
|
||||
key: 'lecturer_id',
|
||||
label: 'Dosen',
|
||||
options: lecturers.map((lecturer) => ({
|
||||
value: String(lecturer.id),
|
||||
label: lecturerLabel(lecturer),
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
const pagination: PaginationState = {
|
||||
current_page: logs.current_page,
|
||||
last_page: logs.last_page,
|
||||
@ -76,9 +93,11 @@ export default function AcademicAdvisingLogIndex({
|
||||
handlePageChange,
|
||||
handlePerPageChange,
|
||||
handleSearchChange,
|
||||
applyFilters,
|
||||
} = useServerTable({
|
||||
route: () => academicAdvisingLogIndex.url(),
|
||||
pagination,
|
||||
filters,
|
||||
});
|
||||
|
||||
function handleDelete() {
|
||||
@ -154,6 +173,13 @@ export default function AcademicAdvisingLogIndex({
|
||||
searchValue={search}
|
||||
searchKey="student"
|
||||
searchPlaceholder="Cari nama atau NIM mahasiswa..."
|
||||
toolbar={
|
||||
<FilterDialog
|
||||
fields={filterFields}
|
||||
activeFilters={filters}
|
||||
onApply={applyFilters}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<DeleteConfirmDialog
|
||||
|
||||
@ -5,6 +5,8 @@ import type { PaginationState } from '@/components/data-table';
|
||||
import { DataTable } from '@/components/data-table';
|
||||
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';
|
||||
@ -43,6 +45,9 @@ type Props = {
|
||||
};
|
||||
students: LetterRequestStudent[];
|
||||
highlight?: number;
|
||||
filters: {
|
||||
status?: string;
|
||||
};
|
||||
};
|
||||
|
||||
function studentLabel(student: LetterRequestStudent): string {
|
||||
@ -53,11 +58,23 @@ export default function LetterRequestIndex({
|
||||
letterRequests,
|
||||
students,
|
||||
highlight,
|
||||
filters,
|
||||
}: Props) {
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [editing, setEditing] = useState<LetterRequest | null>(null);
|
||||
const [deleting, setDeleting] = useState<LetterRequest | null>(null);
|
||||
|
||||
const filterFields: FilterField[] = [
|
||||
{
|
||||
key: 'status',
|
||||
label: 'Status',
|
||||
options: LetterStatuses.map((status) => ({
|
||||
value: status,
|
||||
label: LetterStatusLabels[status],
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
const pagination: PaginationState = {
|
||||
current_page: letterRequests.current_page,
|
||||
last_page: letterRequests.last_page,
|
||||
@ -70,9 +87,11 @@ export default function LetterRequestIndex({
|
||||
handlePageChange,
|
||||
handlePerPageChange,
|
||||
handleSearchChange,
|
||||
applyFilters,
|
||||
} = useServerTable({
|
||||
route: () => letterRequestIndex.url(),
|
||||
pagination,
|
||||
filters,
|
||||
});
|
||||
|
||||
function handleDelete() {
|
||||
@ -146,6 +165,13 @@ export default function LetterRequestIndex({
|
||||
searchValue={search}
|
||||
searchKey="student"
|
||||
searchPlaceholder="Cari nama, NIM, atau jenis surat..."
|
||||
toolbar={
|
||||
<FilterDialog
|
||||
fields={filterFields}
|
||||
activeFilters={filters}
|
||||
onApply={applyFilters}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<DeleteConfirmDialog
|
||||
|
||||
@ -5,6 +5,8 @@ import { ConfirmDialog } from '@/components/confirm-dialog';
|
||||
import type { PaginationState } from '@/components/data-table';
|
||||
import { DataTable } from '@/components/data-table';
|
||||
import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog';
|
||||
import type { FilterField } from '@/components/filter-dialog';
|
||||
import { FilterDialog } from '@/components/filter-dialog';
|
||||
import { PageHeader } from '@/components/page-header';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useServerTable } from '@/hooks/use-server-table';
|
||||
@ -13,7 +15,7 @@ import {
|
||||
create,
|
||||
destroy,
|
||||
edit,
|
||||
reset_password
|
||||
reset_password,
|
||||
} from '@/routes/admin/users/administrators';
|
||||
import type { Administrator } from './columns';
|
||||
import { createAdministratorColumns } from './columns';
|
||||
@ -26,9 +28,23 @@ type Props = {
|
||||
per_page: number;
|
||||
total: number;
|
||||
};
|
||||
filters: {
|
||||
gender?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export default function AdministratorIndex({ administrators }: Props) {
|
||||
const filterFields: FilterField[] = [
|
||||
{
|
||||
key: 'gender',
|
||||
label: 'Jenis Kelamin',
|
||||
options: [
|
||||
{ value: 'male', label: 'Laki-laki' },
|
||||
{ value: 'female', label: 'Perempuan' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default function AdministratorIndex({ administrators, filters }: Props) {
|
||||
const [deleting, setDeleting] = useState<Administrator | null>(null);
|
||||
const [resetting, setResetting] = useState<Administrator | null>(null);
|
||||
|
||||
@ -44,9 +60,11 @@ export default function AdministratorIndex({ administrators }: Props) {
|
||||
handlePageChange,
|
||||
handlePerPageChange,
|
||||
handleSearchChange,
|
||||
applyFilters,
|
||||
} = useServerTable({
|
||||
route: () => administratorsIndex.url(),
|
||||
pagination,
|
||||
filters,
|
||||
});
|
||||
|
||||
function handleDelete() {
|
||||
@ -64,9 +82,13 @@ export default function AdministratorIndex({ administrators }: Props) {
|
||||
return;
|
||||
}
|
||||
|
||||
router.patch(reset_password.url(resetting.id), {}, {
|
||||
onSuccess: () => setResetting(null),
|
||||
});
|
||||
router.patch(
|
||||
reset_password.url(resetting.id),
|
||||
{},
|
||||
{
|
||||
onSuccess: () => setResetting(null),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const columns = createAdministratorColumns({
|
||||
@ -105,6 +127,13 @@ export default function AdministratorIndex({ administrators }: Props) {
|
||||
onPerPageChange={handlePerPageChange}
|
||||
onSearchChange={handleSearchChange}
|
||||
searchValue={search}
|
||||
toolbar={
|
||||
<FilterDialog
|
||||
fields={filterFields}
|
||||
activeFilters={filters}
|
||||
onApply={applyFilters}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<DeleteConfirmDialog
|
||||
|
||||
@ -5,6 +5,8 @@ import { ConfirmDialog } from '@/components/confirm-dialog';
|
||||
import type { PaginationState } from '@/components/data-table';
|
||||
import { DataTable } from '@/components/data-table';
|
||||
import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog';
|
||||
import type { FilterField } from '@/components/filter-dialog';
|
||||
import { FilterDialog } from '@/components/filter-dialog';
|
||||
import { PageHeader } from '@/components/page-header';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useServerTable } from '@/hooks/use-server-table';
|
||||
@ -18,6 +20,8 @@ import {
|
||||
import type { Lecturer } from './columns';
|
||||
import { createLecturerColumns } from './columns';
|
||||
|
||||
type Department = { id: number; name: string };
|
||||
|
||||
type Props = {
|
||||
lecturers: {
|
||||
data: Lecturer[];
|
||||
@ -26,12 +30,40 @@ type Props = {
|
||||
per_page: number;
|
||||
total: number;
|
||||
};
|
||||
departments: Department[];
|
||||
filters: {
|
||||
gender?: string;
|
||||
department_id?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export default function LecturerIndex({ lecturers }: Props) {
|
||||
export default function LecturerIndex({
|
||||
lecturers,
|
||||
departments,
|
||||
filters,
|
||||
}: Props) {
|
||||
const [deleting, setDeleting] = useState<Lecturer | null>(null);
|
||||
const [resetting, setResetting] = useState<Lecturer | null>(null);
|
||||
|
||||
const filterFields: FilterField[] = [
|
||||
{
|
||||
key: 'gender',
|
||||
label: 'Jenis Kelamin',
|
||||
options: [
|
||||
{ value: 'male', label: 'Laki-laki' },
|
||||
{ value: 'female', label: 'Perempuan' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'department_id',
|
||||
label: 'Jurusan',
|
||||
options: departments.map((department) => ({
|
||||
value: String(department.id),
|
||||
label: department.name,
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
const pagination: PaginationState = {
|
||||
current_page: lecturers.current_page,
|
||||
last_page: lecturers.last_page,
|
||||
@ -44,9 +76,11 @@ export default function LecturerIndex({ lecturers }: Props) {
|
||||
handlePageChange,
|
||||
handlePerPageChange,
|
||||
handleSearchChange,
|
||||
applyFilters,
|
||||
} = useServerTable({
|
||||
route: () => lecturersIndex.url(),
|
||||
pagination,
|
||||
filters,
|
||||
});
|
||||
|
||||
function handleDelete() {
|
||||
@ -64,9 +98,13 @@ export default function LecturerIndex({ lecturers }: Props) {
|
||||
return;
|
||||
}
|
||||
|
||||
router.patch(reset_password.url(resetting.id), {}, {
|
||||
onSuccess: () => setResetting(null),
|
||||
});
|
||||
router.patch(
|
||||
reset_password.url(resetting.id),
|
||||
{},
|
||||
{
|
||||
onSuccess: () => setResetting(null),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const columns = createLecturerColumns({
|
||||
@ -105,6 +143,13 @@ export default function LecturerIndex({ lecturers }: Props) {
|
||||
onPerPageChange={handlePerPageChange}
|
||||
onSearchChange={handleSearchChange}
|
||||
searchValue={search}
|
||||
toolbar={
|
||||
<FilterDialog
|
||||
fields={filterFields}
|
||||
activeFilters={filters}
|
||||
onApply={applyFilters}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<DeleteConfirmDialog
|
||||
|
||||
@ -5,6 +5,8 @@ import { ConfirmDialog } from '@/components/confirm-dialog';
|
||||
import type { PaginationState } from '@/components/data-table';
|
||||
import { DataTable } from '@/components/data-table';
|
||||
import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog';
|
||||
import type { FilterField } from '@/components/filter-dialog';
|
||||
import { FilterDialog } from '@/components/filter-dialog';
|
||||
import { PageHeader } from '@/components/page-header';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useServerTable } from '@/hooks/use-server-table';
|
||||
@ -15,8 +17,11 @@ import {
|
||||
reset_password,
|
||||
index as studentsIndex,
|
||||
} from '@/routes/admin/users/students';
|
||||
import { createStudentColumns } from './columns';
|
||||
import type {Student} from './columns';
|
||||
import { createStudentColumns } from './columns';
|
||||
import type { Student } from './columns';
|
||||
|
||||
type Department = { id: number; name: string };
|
||||
type StatusOption = { value: string; label: string };
|
||||
|
||||
type Props = {
|
||||
students: {
|
||||
@ -26,12 +31,48 @@ type Props = {
|
||||
per_page: number;
|
||||
total: number;
|
||||
};
|
||||
departments: Department[];
|
||||
statuses: StatusOption[];
|
||||
filters: {
|
||||
gender?: string;
|
||||
department_id?: string;
|
||||
status?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export default function StudentIndex({ students }: Props) {
|
||||
export default function StudentIndex({
|
||||
students,
|
||||
departments,
|
||||
statuses,
|
||||
filters,
|
||||
}: Props) {
|
||||
const [deleting, setDeleting] = useState<Student | null>(null);
|
||||
const [resetting, setResetting] = useState<Student | null>(null);
|
||||
|
||||
const filterFields: FilterField[] = [
|
||||
{
|
||||
key: 'gender',
|
||||
label: 'Jenis Kelamin',
|
||||
options: [
|
||||
{ value: 'male', label: 'Laki-laki' },
|
||||
{ value: 'female', label: 'Perempuan' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'department_id',
|
||||
label: 'Jurusan',
|
||||
options: departments.map((department) => ({
|
||||
value: String(department.id),
|
||||
label: department.name,
|
||||
})),
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
label: 'Status',
|
||||
options: statuses,
|
||||
},
|
||||
];
|
||||
|
||||
const pagination: PaginationState = {
|
||||
current_page: students.current_page,
|
||||
last_page: students.last_page,
|
||||
@ -44,9 +85,11 @@ export default function StudentIndex({ students }: Props) {
|
||||
handlePageChange,
|
||||
handlePerPageChange,
|
||||
handleSearchChange,
|
||||
applyFilters,
|
||||
} = useServerTable({
|
||||
route: () => studentsIndex.url(),
|
||||
pagination,
|
||||
filters,
|
||||
});
|
||||
|
||||
function handleDelete() {
|
||||
@ -64,9 +107,13 @@ export default function StudentIndex({ students }: Props) {
|
||||
return;
|
||||
}
|
||||
|
||||
router.patch(reset_password.url(resetting.id), {}, {
|
||||
onSuccess: () => setResetting(null),
|
||||
});
|
||||
router.patch(
|
||||
reset_password.url(resetting.id),
|
||||
{},
|
||||
{
|
||||
onSuccess: () => setResetting(null),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const columns = createStudentColumns({
|
||||
@ -105,6 +152,13 @@ export default function StudentIndex({ students }: Props) {
|
||||
onPerPageChange={handlePerPageChange}
|
||||
onSearchChange={handleSearchChange}
|
||||
searchValue={search}
|
||||
toolbar={
|
||||
<FilterDialog
|
||||
fields={filterFields}
|
||||
activeFilters={filters}
|
||||
onApply={applyFilters}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<DeleteConfirmDialog
|
||||
|
||||
Loading…
Reference in New Issue
Block a user