feat: implement infinite scroll for various admin pages and update pagination handling
- Refactored AdministratorController, LecturerController, and StudentController to utilize Inertia's scroll feature for paginated data. - Enhanced DataCards and DataTable components to support infinite scrolling, allowing for a smoother user experience when navigating large datasets. - Updated multiple admin pages (e.g., MaterialIndex, LogIndex, FeedbackIndex) to integrate infinite scroll functionality, improving data loading efficiency. - Adjusted server table hooks to reset keys appropriately for infinite scroll scenarios.
This commit is contained in:
parent
33f354ad94
commit
35a64c2045
@ -22,11 +22,11 @@ public function __construct(
|
|||||||
public function index(PaginatedRequest $request): Response
|
public function index(PaginatedRequest $request): Response
|
||||||
{
|
{
|
||||||
return Inertia::render('admin/academic-classes/materials/index', [
|
return Inertia::render('admin/academic-classes/materials/index', [
|
||||||
'materials' => $this->service->paginated(
|
'materials' => Inertia::scroll(fn () => $this->service->paginated(
|
||||||
$request->user(),
|
$request->user(),
|
||||||
...$request->validatedWithDefaults(),
|
...$request->validatedWithDefaults(),
|
||||||
courseClassId: $request->validated('course_class_id'),
|
courseClassId: $request->validated('course_class_id'),
|
||||||
),
|
)),
|
||||||
'courseClasses' => $this->courseClassService->getAllForSelect($request->user()),
|
'courseClasses' => $this->courseClassService->getAllForSelect($request->user()),
|
||||||
'filters' => $request->only(['course_class_id']),
|
'filters' => $request->only(['course_class_id']),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -22,11 +22,11 @@ public function index(PaginatedRequest $request): Response
|
|||||||
|
|
||||||
return Inertia::render('admin/developer/logs/index', [
|
return Inertia::render('admin/developer/logs/index', [
|
||||||
'entries' => $file
|
'entries' => $file
|
||||||
? $this->service->paginated(
|
? Inertia::scroll(fn () => $this->service->paginated(
|
||||||
$file,
|
$file,
|
||||||
...$request->validatedWithDefaults(),
|
...$request->validatedWithDefaults(),
|
||||||
level: $request->validated('level'),
|
level: $request->validated('level'),
|
||||||
)
|
))
|
||||||
: null,
|
: null,
|
||||||
'files' => $files,
|
'files' => $files,
|
||||||
'selectedFile' => $file,
|
'selectedFile' => $file,
|
||||||
|
|||||||
@ -25,12 +25,12 @@ public function __construct(
|
|||||||
public function index(PaginatedRequest $request): Response
|
public function index(PaginatedRequest $request): Response
|
||||||
{
|
{
|
||||||
return Inertia::render('admin/feedback/index', [
|
return Inertia::render('admin/feedback/index', [
|
||||||
'feedbacks' => $this->service->paginated(
|
'feedbacks' => Inertia::scroll(fn () => $this->service->paginated(
|
||||||
$request->user(),
|
$request->user(),
|
||||||
...$request->validatedWithDefaults(),
|
...$request->validatedWithDefaults(),
|
||||||
type: $request->validated('type'),
|
type: $request->validated('type'),
|
||||||
status: $request->validated('status'),
|
status: $request->validated('status'),
|
||||||
),
|
)),
|
||||||
'types' => FeedbackType::options(),
|
'types' => FeedbackType::options(),
|
||||||
'statuses' => FeedbackStatus::options(),
|
'statuses' => FeedbackStatus::options(),
|
||||||
'filters' => $request->only(['type', 'status']),
|
'filters' => $request->only(['type', 'status']),
|
||||||
|
|||||||
@ -38,12 +38,12 @@ public function index(PaginatedRequest $request): Response
|
|||||||
$summaryTermId = $requestedTermId ?? $this->academicTermService->getActive()?->id;
|
$summaryTermId = $requestedTermId ?? $this->academicTermService->getActive()?->id;
|
||||||
|
|
||||||
return Inertia::render('admin/finances/tuition-invoices/index', [
|
return Inertia::render('admin/finances/tuition-invoices/index', [
|
||||||
'invoices' => $this->service->paginated(
|
'invoices' => Inertia::scroll(fn () => $this->service->paginated(
|
||||||
...$request->validatedWithDefaults(),
|
...$request->validatedWithDefaults(),
|
||||||
academicTermId: $requestedTermId,
|
academicTermId: $requestedTermId,
|
||||||
status: $request->validated('status'),
|
status: $request->validated('status'),
|
||||||
paymentMethod: $request->validated('payment_method'),
|
paymentMethod: $request->validated('payment_method'),
|
||||||
),
|
)),
|
||||||
'summary' => $this->service->summary(
|
'summary' => $this->service->summary(
|
||||||
search: $request->validated('search') ?? '',
|
search: $request->validated('search') ?? '',
|
||||||
academicTermId: $summaryTermId,
|
academicTermId: $summaryTermId,
|
||||||
|
|||||||
@ -20,10 +20,10 @@ public function __construct(
|
|||||||
public function index(PaginatedRequest $request): Response
|
public function index(PaginatedRequest $request): Response
|
||||||
{
|
{
|
||||||
return Inertia::render('admin/manage/announcements/index', [
|
return Inertia::render('admin/manage/announcements/index', [
|
||||||
'announcements' => $this->service->paginated(
|
'announcements' => Inertia::scroll(fn () => $this->service->paginated(
|
||||||
$request->user(),
|
$request->user(),
|
||||||
...$request->validatedWithDefaults(),
|
...$request->validatedWithDefaults(),
|
||||||
),
|
)),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -27,11 +27,11 @@ public function __construct(
|
|||||||
public function index(PaginatedRequest $request): Response
|
public function index(PaginatedRequest $request): Response
|
||||||
{
|
{
|
||||||
return Inertia::render('admin/manage/course-classes/index', [
|
return Inertia::render('admin/manage/course-classes/index', [
|
||||||
'courseClasses' => $this->service->paginated(
|
'courseClasses' => Inertia::scroll(fn () => $this->service->paginated(
|
||||||
...$request->validatedWithDefaults(),
|
...$request->validatedWithDefaults(),
|
||||||
academicTermId: $request->validated('academic_term_id'),
|
academicTermId: $request->validated('academic_term_id'),
|
||||||
method: $request->validated('method'),
|
method: $request->validated('method'),
|
||||||
),
|
)),
|
||||||
'courses' => $this->courseService->getAllForSelect(),
|
'courses' => $this->courseService->getAllForSelect(),
|
||||||
'lecturers' => $this->lecturerService->getAllForSelect(),
|
'lecturers' => $this->lecturerService->getAllForSelect(),
|
||||||
'academicTerms' => $this->academicTermService->getAllForSelect(),
|
'academicTerms' => $this->academicTermService->getAllForSelect(),
|
||||||
|
|||||||
@ -35,11 +35,11 @@ public function index(PaginatedRequest $request): Response|RedirectResponse
|
|||||||
|
|
||||||
return Inertia::render('admin/manage/course-registrations/index', [
|
return Inertia::render('admin/manage/course-registrations/index', [
|
||||||
'departments' => $this->service->departmentSummary(),
|
'departments' => $this->service->departmentSummary(),
|
||||||
'registrations' => $this->service->paginated(
|
'registrations' => Inertia::scroll(fn () => $this->service->paginated(
|
||||||
$user,
|
$user,
|
||||||
...$request->validatedWithDefaults(),
|
...$request->validatedWithDefaults(),
|
||||||
departmentId: $request->validated('department_id'),
|
departmentId: $request->validated('department_id'),
|
||||||
),
|
)),
|
||||||
'filters' => $request->only(['department_id']),
|
'filters' => $request->only(['department_id']),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,11 +21,11 @@ public function __construct(
|
|||||||
public function index(PaginatedRequest $request): Response
|
public function index(PaginatedRequest $request): Response
|
||||||
{
|
{
|
||||||
return Inertia::render('admin/master/academic-terms/index', [
|
return Inertia::render('admin/master/academic-terms/index', [
|
||||||
'academicTerms' => $this->service->paginated(
|
'academicTerms' => Inertia::scroll(fn () => $this->service->paginated(
|
||||||
...$request->validatedWithDefaults(),
|
...$request->validatedWithDefaults(),
|
||||||
semester: $request->validated('semester'),
|
semester: $request->validated('semester'),
|
||||||
isActive: $request->filled('is_active') ? $request->boolean('is_active') : null,
|
isActive: $request->filled('is_active') ? $request->boolean('is_active') : null,
|
||||||
),
|
)),
|
||||||
'filters' => $request->only(['semester', 'is_active']),
|
'filters' => $request->only(['semester', 'is_active']),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,12 +22,12 @@ public function __construct(
|
|||||||
public function index(PaginatedRequest $request): Response
|
public function index(PaginatedRequest $request): Response
|
||||||
{
|
{
|
||||||
return Inertia::render('admin/master/courses/index', [
|
return Inertia::render('admin/master/courses/index', [
|
||||||
'courses' => $this->service->paginated(
|
'courses' => Inertia::scroll(fn () => $this->service->paginated(
|
||||||
$request->user(),
|
$request->user(),
|
||||||
...$request->validatedWithDefaults(),
|
...$request->validatedWithDefaults(),
|
||||||
departmentId: $request->validated('department_id'),
|
departmentId: $request->validated('department_id'),
|
||||||
semesterNumber: $request->validated('semester_number'),
|
semesterNumber: $request->validated('semester_number'),
|
||||||
),
|
)),
|
||||||
'departments' => $this->departmentService->getAllForSelect(),
|
'departments' => $this->departmentService->getAllForSelect(),
|
||||||
'semesterNumbers' => $this->service->getSemesterNumbers(),
|
'semesterNumbers' => $this->service->getSemesterNumbers(),
|
||||||
'filters' => $request->only(['department_id', 'semester_number']),
|
'filters' => $request->only(['department_id', 'semester_number']),
|
||||||
|
|||||||
@ -22,7 +22,7 @@ public function __construct(
|
|||||||
public function index(PaginatedRequest $request): Response
|
public function index(PaginatedRequest $request): Response
|
||||||
{
|
{
|
||||||
return Inertia::render('admin/master/departments/index', [
|
return Inertia::render('admin/master/departments/index', [
|
||||||
'departments' => $this->service->paginated(...$request->validatedWithDefaults()),
|
'departments' => Inertia::scroll(fn () => $this->service->paginated(...$request->validatedWithDefaults())),
|
||||||
'lecturers' => $this->lecturerService->getAllForSelect(),
|
'lecturers' => $this->lecturerService->getAllForSelect(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,11 +24,11 @@ public function __construct(
|
|||||||
public function index(PaginatedRequest $request): Response
|
public function index(PaginatedRequest $request): Response
|
||||||
{
|
{
|
||||||
return Inertia::render('admin/services/academic-advising-logs/index', [
|
return Inertia::render('admin/services/academic-advising-logs/index', [
|
||||||
'logs' => $this->service->paginated(
|
'logs' => Inertia::scroll(fn () => $this->service->paginated(
|
||||||
$request->user(),
|
$request->user(),
|
||||||
...$request->validatedWithDefaults(),
|
...$request->validatedWithDefaults(),
|
||||||
lecturerId: $request->validated('lecturer_id'),
|
lecturerId: $request->validated('lecturer_id'),
|
||||||
),
|
)),
|
||||||
'students' => $this->studentService->getAllForSelect(),
|
'students' => $this->studentService->getAllForSelect(),
|
||||||
'lecturers' => $this->lecturerService->getAllForSelect(),
|
'lecturers' => $this->lecturerService->getAllForSelect(),
|
||||||
'filters' => $request->only(['lecturer_id']),
|
'filters' => $request->only(['lecturer_id']),
|
||||||
|
|||||||
@ -22,11 +22,11 @@ public function __construct(
|
|||||||
public function index(PaginatedRequest $request): Response
|
public function index(PaginatedRequest $request): Response
|
||||||
{
|
{
|
||||||
return Inertia::render('admin/services/letter-requests/index', [
|
return Inertia::render('admin/services/letter-requests/index', [
|
||||||
'letterRequests' => $this->service->paginated(
|
'letterRequests' => Inertia::scroll(fn () => $this->service->paginated(
|
||||||
$request->user(),
|
$request->user(),
|
||||||
...$request->validatedWithDefaults(),
|
...$request->validatedWithDefaults(),
|
||||||
status: $request->validated('status'),
|
status: $request->validated('status'),
|
||||||
),
|
)),
|
||||||
'filters' => $request->only(['status']),
|
'filters' => $request->only(['status']),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,10 +21,10 @@ public function __construct(
|
|||||||
public function index(PaginatedRequest $request): Response
|
public function index(PaginatedRequest $request): Response
|
||||||
{
|
{
|
||||||
return Inertia::render('admin/users/administrators/index', [
|
return Inertia::render('admin/users/administrators/index', [
|
||||||
'administrators' => $this->service->paginated(
|
'administrators' => Inertia::scroll(fn () => $this->service->paginated(
|
||||||
...$request->validatedWithDefaults(),
|
...$request->validatedWithDefaults(),
|
||||||
gender: $request->validated('gender'),
|
gender: $request->validated('gender'),
|
||||||
),
|
)),
|
||||||
'filters' => $request->only(['gender']),
|
'filters' => $request->only(['gender']),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,11 +26,11 @@ public function __construct(
|
|||||||
public function index(PaginatedRequest $request): Response
|
public function index(PaginatedRequest $request): Response
|
||||||
{
|
{
|
||||||
return Inertia::render('admin/users/lecturers/index', [
|
return Inertia::render('admin/users/lecturers/index', [
|
||||||
'lecturers' => $this->service->paginated(
|
'lecturers' => Inertia::scroll(fn () => $this->service->paginated(
|
||||||
...$request->validatedWithDefaults(),
|
...$request->validatedWithDefaults(),
|
||||||
gender: $request->validated('gender'),
|
gender: $request->validated('gender'),
|
||||||
departmentId: $request->validated('department_id'),
|
departmentId: $request->validated('department_id'),
|
||||||
),
|
)),
|
||||||
'departments' => $this->departmentService->getAllForSelect(),
|
'departments' => $this->departmentService->getAllForSelect(),
|
||||||
'filters' => $request->only(['gender', 'department_id']),
|
'filters' => $request->only(['gender', 'department_id']),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -30,14 +30,14 @@ public function __construct(
|
|||||||
public function index(PaginatedRequest $request): Response
|
public function index(PaginatedRequest $request): Response
|
||||||
{
|
{
|
||||||
return Inertia::render('admin/users/students/index', [
|
return Inertia::render('admin/users/students/index', [
|
||||||
'students' => $this->service->paginated(
|
'students' => Inertia::scroll(fn () => $this->service->paginated(
|
||||||
$request->user(),
|
$request->user(),
|
||||||
...$request->validatedWithDefaults(),
|
...$request->validatedWithDefaults(),
|
||||||
gender: $request->validated('gender'),
|
gender: $request->validated('gender'),
|
||||||
departmentId: $request->validated('department_id'),
|
departmentId: $request->validated('department_id'),
|
||||||
status: $request->validated('status'),
|
status: $request->validated('status'),
|
||||||
enrollmentYear: $request->validated('enrollment_year'),
|
enrollmentYear: $request->validated('enrollment_year'),
|
||||||
),
|
)),
|
||||||
'departments' => $this->departmentService->getAllForSelect(),
|
'departments' => $this->departmentService->getAllForSelect(),
|
||||||
'statuses' => StudentStatus::options(),
|
'statuses' => StudentStatus::options(),
|
||||||
'enrollmentYears' => $this->service->getEnrollmentYears(),
|
'enrollmentYears' => $this->service->getEnrollmentYears(),
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { InfiniteScroll } from '@inertiajs/react';
|
||||||
import {
|
import {
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
@ -29,10 +30,19 @@ interface DataCardsProps<TData> {
|
|||||||
emptyText?: string;
|
emptyText?: string;
|
||||||
toolbar?: React.ReactNode;
|
toolbar?: React.ReactNode;
|
||||||
pagination: PaginationState;
|
pagination: PaginationState;
|
||||||
onPageChange: (page: number) => void;
|
onPageChange?: (page: number) => void;
|
||||||
onPerPageChange?: (perPage: number) => void;
|
onPerPageChange?: (perPage: number) => void;
|
||||||
onSearchChange?: (search: string) => void;
|
onSearchChange?: (search: string) => void;
|
||||||
searchValue?: string;
|
searchValue?: string;
|
||||||
|
/**
|
||||||
|
* Enables infinite scroll instead of page-based pagination. `propName`
|
||||||
|
* must match the Inertia page prop returned via `Inertia::scroll()` on
|
||||||
|
* the backend so the frontend can track its merge/scroll metadata.
|
||||||
|
*/
|
||||||
|
infiniteScroll?: {
|
||||||
|
propName: string;
|
||||||
|
buffer?: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DataCards<TData>({
|
export function DataCards<TData>({
|
||||||
@ -48,7 +58,9 @@ export function DataCards<TData>({
|
|||||||
onPerPageChange,
|
onPerPageChange,
|
||||||
onSearchChange,
|
onSearchChange,
|
||||||
searchValue,
|
searchValue,
|
||||||
|
infiniteScroll,
|
||||||
}: DataCardsProps<TData>) {
|
}: DataCardsProps<TData>) {
|
||||||
|
const isInfiniteScroll = !!infiniteScroll;
|
||||||
const [localSearch, setLocalSearch] = React.useState(searchValue ?? '');
|
const [localSearch, setLocalSearch] = React.useState(searchValue ?? '');
|
||||||
const [previousSearchValue, setPreviousSearchValue] =
|
const [previousSearchValue, setPreviousSearchValue] =
|
||||||
React.useState(searchValue);
|
React.useState(searchValue);
|
||||||
@ -112,16 +124,51 @@ export function DataCards<TData>({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{data.length ? (
|
{data.length ? (
|
||||||
|
isInfiniteScroll ? (
|
||||||
|
<InfiniteScroll
|
||||||
|
data={infiniteScroll.propName}
|
||||||
|
as="div"
|
||||||
|
onlyNext
|
||||||
|
preserveUrl
|
||||||
|
buffer={infiniteScroll.buffer ?? 300}
|
||||||
|
className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
|
||||||
|
next={({ loading, hasMore }) =>
|
||||||
|
loading ? (
|
||||||
|
<p className="py-2 text-center text-sm text-muted-foreground">
|
||||||
|
Memuat data...
|
||||||
|
</p>
|
||||||
|
) : hasMore ? null : (
|
||||||
|
<p className="py-2 text-center text-sm text-muted-foreground">
|
||||||
|
Semua data telah ditampilkan.
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{data.map((item, index) => (
|
||||||
|
<Card
|
||||||
|
key={getRowId ? getRowId(item) : index}
|
||||||
|
className="overflow-hidden"
|
||||||
|
>
|
||||||
|
<CardContent>
|
||||||
|
{renderCard(item, index)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</InfiniteScroll>
|
||||||
|
) : (
|
||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||||
{data.map((item, index) => (
|
{data.map((item, index) => (
|
||||||
<Card
|
<Card
|
||||||
key={getRowId ? getRowId(item) : index}
|
key={getRowId ? getRowId(item) : index}
|
||||||
className="overflow-hidden"
|
className="overflow-hidden"
|
||||||
>
|
>
|
||||||
<CardContent>{renderCard(item, index)}</CardContent>
|
<CardContent>
|
||||||
|
{renderCard(item, index)}
|
||||||
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="flex h-24 items-center justify-center text-center text-muted-foreground">
|
<CardContent className="flex h-24 items-center justify-center text-center text-muted-foreground">
|
||||||
@ -130,6 +177,12 @@ export function DataCards<TData>({
|
|||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{isInfiniteScroll ? (
|
||||||
|
<p className="text-center text-sm text-muted-foreground">
|
||||||
|
Menampilkan {data.length.toLocaleString('id-ID')} dari{' '}
|
||||||
|
{pagination.total.toLocaleString('id-ID')} data
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
<div className="flex items-center justify-end gap-2">
|
<div className="flex items-center justify-end gap-2">
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
{`Halaman ${currentPage} dari ${totalPages}`}
|
{`Halaman ${currentPage} dari ${totalPages}`}
|
||||||
@ -138,7 +191,7 @@ export function DataCards<TData>({
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => onPageChange(1)}
|
onClick={() => onPageChange?.(1)}
|
||||||
disabled={currentPage <= 1}
|
disabled={currentPage <= 1}
|
||||||
>
|
>
|
||||||
<ChevronsLeft className="h-4 w-4" />
|
<ChevronsLeft className="h-4 w-4" />
|
||||||
@ -146,7 +199,7 @@ export function DataCards<TData>({
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => onPageChange(currentPage - 1)}
|
onClick={() => onPageChange?.(currentPage - 1)}
|
||||||
disabled={currentPage <= 1}
|
disabled={currentPage <= 1}
|
||||||
>
|
>
|
||||||
<ChevronLeft className="h-4 w-4" />
|
<ChevronLeft className="h-4 w-4" />
|
||||||
@ -154,7 +207,7 @@ export function DataCards<TData>({
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => onPageChange(currentPage + 1)}
|
onClick={() => onPageChange?.(currentPage + 1)}
|
||||||
disabled={currentPage >= totalPages}
|
disabled={currentPage >= totalPages}
|
||||||
>
|
>
|
||||||
<ChevronRight className="h-4 w-4" />
|
<ChevronRight className="h-4 w-4" />
|
||||||
@ -162,13 +215,14 @@ export function DataCards<TData>({
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => onPageChange(totalPages)}
|
onClick={() => onPageChange?.(totalPages)}
|
||||||
disabled={currentPage >= totalPages}
|
disabled={currentPage >= totalPages}
|
||||||
>
|
>
|
||||||
<ChevronsRight className="h-4 w-4" />
|
<ChevronsRight className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {
|
|||||||
verticalListSortingStrategy,
|
verticalListSortingStrategy,
|
||||||
} from '@dnd-kit/sortable';
|
} from '@dnd-kit/sortable';
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
|
import { InfiniteScroll } from '@inertiajs/react';
|
||||||
import type { ColumnDef, ExpandedState, Row } from '@tanstack/react-table';
|
import type { ColumnDef, ExpandedState, Row } from '@tanstack/react-table';
|
||||||
import {
|
import {
|
||||||
flexRender,
|
flexRender,
|
||||||
@ -71,6 +72,15 @@ interface DataTableProps<TData, TValue> {
|
|||||||
onPerPageChange?: (perPage: number) => void;
|
onPerPageChange?: (perPage: number) => void;
|
||||||
onSearchChange?: (search: string) => void;
|
onSearchChange?: (search: string) => void;
|
||||||
searchValue?: string;
|
searchValue?: string;
|
||||||
|
/**
|
||||||
|
* Enables infinite scroll instead of page-based pagination. `propName`
|
||||||
|
* must match the Inertia page prop returned via `Inertia::scroll()` on
|
||||||
|
* the backend so the frontend can track its merge/scroll metadata.
|
||||||
|
*/
|
||||||
|
infiniteScroll?: {
|
||||||
|
propName: string;
|
||||||
|
buffer?: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const DragHandleContext = React.createContext<{
|
const DragHandleContext = React.createContext<{
|
||||||
@ -163,7 +173,10 @@ export function DataTable<TData, TValue>({
|
|||||||
onPerPageChange,
|
onPerPageChange,
|
||||||
onSearchChange,
|
onSearchChange,
|
||||||
searchValue,
|
searchValue,
|
||||||
|
infiniteScroll,
|
||||||
}: DataTableProps<TData, TValue>) {
|
}: DataTableProps<TData, TValue>) {
|
||||||
|
const tbodyRef = React.useRef<HTMLTableSectionElement>(null);
|
||||||
|
|
||||||
const [expanded, setExpanded] = React.useState<ExpandedState>(() => {
|
const [expanded, setExpanded] = React.useState<ExpandedState>(() => {
|
||||||
if (!defaultExpanded || !data.length) {
|
if (!defaultExpanded || !data.length) {
|
||||||
return {};
|
return {};
|
||||||
@ -183,7 +196,9 @@ export function DataTable<TData, TValue>({
|
|||||||
setLocalSearch(searchValue ?? '');
|
setLocalSearch(searchValue ?? '');
|
||||||
}, [searchValue]);
|
}, [searchValue]);
|
||||||
|
|
||||||
const isServerMode = !!pagination && !!onPageChange;
|
const isInfiniteScroll = !!infiniteScroll;
|
||||||
|
const hasServerPager = !isInfiniteScroll && !!pagination && !!onPageChange;
|
||||||
|
const hasServerSearch = !!onSearchChange;
|
||||||
const isSortable = !!onReorder && !!getRowId;
|
const isSortable = !!onReorder && !!getRowId;
|
||||||
|
|
||||||
const handleSearchDebounced = useDebounce(
|
const handleSearchDebounced = useDebounce(
|
||||||
@ -200,7 +215,7 @@ export function DataTable<TData, TValue>({
|
|||||||
header: () => <span className="block text-center">No</span>,
|
header: () => <span className="block text-center">No</span>,
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<span className="block text-center">
|
<span className="block text-center">
|
||||||
{isServerMode
|
{hasServerPager
|
||||||
? (currentPage - 1) * (pagination?.per_page ?? 25) +
|
? (currentPage - 1) * (pagination?.per_page ?? 25) +
|
||||||
row.index +
|
row.index +
|
||||||
1
|
1
|
||||||
@ -275,14 +290,14 @@ export function DataTable<TData, TValue>({
|
|||||||
function handleSearchChange(value: string) {
|
function handleSearchChange(value: string) {
|
||||||
setLocalSearch(value);
|
setLocalSearch(value);
|
||||||
|
|
||||||
if (isServerMode) {
|
if (hasServerSearch) {
|
||||||
handleSearchDebounced(value);
|
handleSearchDebounced(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{(searchKey || toolbar || isServerMode) && (
|
{(searchKey || toolbar || hasServerPager || isInfiniteScroll) && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{searchKey && (
|
{searchKey && (
|
||||||
<Input
|
<Input
|
||||||
@ -296,7 +311,7 @@ export function DataTable<TData, TValue>({
|
|||||||
)}
|
)}
|
||||||
{toolbar}
|
{toolbar}
|
||||||
<div className="ml-auto flex items-center gap-2">
|
<div className="ml-auto flex items-center gap-2">
|
||||||
{isServerMode && onPerPageChange && (
|
{hasServerPager && onPerPageChange && (
|
||||||
<Select
|
<Select
|
||||||
value={String(pagination?.per_page ?? 25)}
|
value={String(pagination?.per_page ?? 25)}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
@ -349,7 +364,7 @@ export function DataTable<TData, TValue>({
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody ref={tbodyRef}>
|
||||||
{table.getRowModel().rows?.length ? (
|
{table.getRowModel().rows?.length ? (
|
||||||
isSortable ? (
|
isSortable ? (
|
||||||
<DndContext
|
<DndContext
|
||||||
@ -407,8 +422,7 @@ export function DataTable<TData, TValue>({
|
|||||||
</DndContext>
|
</DndContext>
|
||||||
) : (
|
) : (
|
||||||
(() => {
|
(() => {
|
||||||
let previousGroup: string | null =
|
let previousGroup: string | null = null;
|
||||||
null;
|
|
||||||
|
|
||||||
return table
|
return table
|
||||||
.getRowModel()
|
.getRowModel()
|
||||||
@ -445,8 +459,7 @@ export function DataTable<TData, TValue>({
|
|||||||
>
|
>
|
||||||
{row
|
{row
|
||||||
.getVisibleCells()
|
.getVisibleCells()
|
||||||
.map(
|
.map((cell) => (
|
||||||
(cell) => (
|
|
||||||
<TableCell
|
<TableCell
|
||||||
key={
|
key={
|
||||||
cell.id
|
cell.id
|
||||||
@ -471,8 +484,7 @@ export function DataTable<TData, TValue>({
|
|||||||
cell.getContext(),
|
cell.getContext(),
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
),
|
))}
|
||||||
)}
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
{renderSubRow &&
|
{renderSubRow &&
|
||||||
row.getIsExpanded() && (
|
row.getIsExpanded() && (
|
||||||
@ -512,14 +524,43 @@ export function DataTable<TData, TValue>({
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{isInfiniteScroll ? (
|
||||||
|
<div className="flex flex-col items-center gap-1">
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
Menampilkan {data.length.toLocaleString('id-ID')} dari{' '}
|
||||||
|
{(pagination?.total ?? data.length).toLocaleString(
|
||||||
|
'id-ID',
|
||||||
|
)}{' '}
|
||||||
|
data
|
||||||
|
</span>
|
||||||
|
<InfiniteScroll
|
||||||
|
data={infiniteScroll.propName}
|
||||||
|
itemsElement={tbodyRef}
|
||||||
|
onlyNext
|
||||||
|
preserveUrl
|
||||||
|
buffer={infiniteScroll.buffer ?? 300}
|
||||||
|
next={({ loading, hasMore }) =>
|
||||||
|
loading ? (
|
||||||
|
<span className="block py-2 text-center text-sm text-muted-foreground">
|
||||||
|
Memuat data...
|
||||||
|
</span>
|
||||||
|
) : hasMore ? null : (
|
||||||
|
<span className="block py-2 text-center text-sm text-muted-foreground">
|
||||||
|
Semua data telah ditampilkan.
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div className="flex items-center justify-end gap-2">
|
<div className="flex items-center justify-end gap-2">
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
{isServerMode
|
{hasServerPager
|
||||||
? `Halaman ${currentPage} dari ${totalPages}`
|
? `Halaman ${currentPage} dari ${totalPages}`
|
||||||
: `Halaman ${table.getState().pagination.pageIndex + 1} dari ${table.getPageCount()}`}
|
: `Halaman ${table.getState().pagination.pageIndex + 1} dari ${table.getPageCount()}`}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
{isServerMode ? (
|
{hasServerPager ? (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@ -532,7 +573,9 @@ export function DataTable<TData, TValue>({
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => onPageChange(currentPage - 1)}
|
onClick={() =>
|
||||||
|
onPageChange(currentPage - 1)
|
||||||
|
}
|
||||||
disabled={currentPage <= 1}
|
disabled={currentPage <= 1}
|
||||||
>
|
>
|
||||||
<ChevronLeft className="h-4 w-4" />
|
<ChevronLeft className="h-4 w-4" />
|
||||||
@ -540,7 +583,9 @@ export function DataTable<TData, TValue>({
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => onPageChange(currentPage + 1)}
|
onClick={() =>
|
||||||
|
onPageChange(currentPage + 1)
|
||||||
|
}
|
||||||
disabled={currentPage >= totalPages}
|
disabled={currentPage >= totalPages}
|
||||||
>
|
>
|
||||||
<ChevronRight className="h-4 w-4" />
|
<ChevronRight className="h-4 w-4" />
|
||||||
@ -584,7 +629,9 @@ export function DataTable<TData, TValue>({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
table.setPageIndex(table.getPageCount() - 1)
|
table.setPageIndex(
|
||||||
|
table.getPageCount() - 1,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
disabled={!table.getCanNextPage()}
|
disabled={!table.getCanNextPage()}
|
||||||
>
|
>
|
||||||
@ -594,6 +641,7 @@ export function DataTable<TData, TValue>({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -120,7 +120,10 @@ function CourseClassField({
|
|||||||
<ComboboxLabel>{group.value}</ComboboxLabel>
|
<ComboboxLabel>{group.value}</ComboboxLabel>
|
||||||
<ComboboxCollection>
|
<ComboboxCollection>
|
||||||
{(option: CourseClassOption) => (
|
{(option: CourseClassOption) => (
|
||||||
<ComboboxItem key={option.id} value={option}>
|
<ComboboxItem
|
||||||
|
key={option.id}
|
||||||
|
value={option}
|
||||||
|
>
|
||||||
{courseClassLabel(option)}
|
{courseClassLabel(option)}
|
||||||
</ComboboxItem>
|
</ComboboxItem>
|
||||||
)}
|
)}
|
||||||
@ -176,16 +179,11 @@ export default function MaterialIndex({
|
|||||||
total: materials.total,
|
total: materials.total,
|
||||||
};
|
};
|
||||||
|
|
||||||
const {
|
const { search, handleSearchChange, applyFilters } = useServerTable({
|
||||||
search,
|
|
||||||
handlePageChange,
|
|
||||||
handlePerPageChange,
|
|
||||||
handleSearchChange,
|
|
||||||
applyFilters,
|
|
||||||
} = useServerTable({
|
|
||||||
route: () => materialIndex.url(),
|
route: () => materialIndex.url(),
|
||||||
pagination,
|
pagination,
|
||||||
filters,
|
filters,
|
||||||
|
resetKeys: ['materials'],
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleDelete() {
|
function handleDelete() {
|
||||||
@ -291,10 +289,9 @@ export default function MaterialIndex({
|
|||||||
data={materials.data}
|
data={materials.data}
|
||||||
searchKey="title"
|
searchKey="title"
|
||||||
pagination={pagination}
|
pagination={pagination}
|
||||||
onPageChange={handlePageChange}
|
|
||||||
onPerPageChange={handlePerPageChange}
|
|
||||||
onSearchChange={handleSearchChange}
|
onSearchChange={handleSearchChange}
|
||||||
searchValue={search}
|
searchValue={search}
|
||||||
|
infiniteScroll={{ propName: 'materials' }}
|
||||||
toolbar={toolbar}
|
toolbar={toolbar}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@ -304,10 +301,9 @@ export default function MaterialIndex({
|
|||||||
getRowId={(material) => material.id}
|
getRowId={(material) => material.id}
|
||||||
searchKey="title"
|
searchKey="title"
|
||||||
pagination={pagination}
|
pagination={pagination}
|
||||||
onPageChange={handlePageChange}
|
|
||||||
onPerPageChange={handlePerPageChange}
|
|
||||||
onSearchChange={handleSearchChange}
|
onSearchChange={handleSearchChange}
|
||||||
searchValue={search}
|
searchValue={search}
|
||||||
|
infiniteScroll={{ propName: 'materials' }}
|
||||||
toolbar={toolbar}
|
toolbar={toolbar}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -83,23 +83,18 @@ export default function LogIndex({
|
|||||||
total: entries?.total ?? 0,
|
total: entries?.total ?? 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const {
|
const { search, handleSearchChange, applyFilters } = useServerTable({
|
||||||
search,
|
|
||||||
handlePageChange,
|
|
||||||
handlePerPageChange,
|
|
||||||
handleSearchChange,
|
|
||||||
applyFilters,
|
|
||||||
} = useServerTable({
|
|
||||||
route: () => logsIndex.url(),
|
route: () => logsIndex.url(),
|
||||||
pagination,
|
pagination,
|
||||||
filters,
|
filters,
|
||||||
|
resetKeys: ['entries'],
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleFileChange(file: string) {
|
function handleFileChange(file: string) {
|
||||||
router.get(
|
router.get(
|
||||||
logsIndex.url(),
|
logsIndex.url(),
|
||||||
{ file, level: filters.level },
|
{ file, level: filters.level },
|
||||||
{ preserveState: true, replace: true },
|
{ preserveState: true, replace: true, reset: ['entries'] },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,13 +149,12 @@ export default function LogIndex({
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
data={entries.data}
|
data={entries.data}
|
||||||
pagination={pagination}
|
pagination={pagination}
|
||||||
onPageChange={handlePageChange}
|
|
||||||
onPerPageChange={handlePerPageChange}
|
|
||||||
onSearchChange={handleSearchChange}
|
onSearchChange={handleSearchChange}
|
||||||
searchValue={search}
|
searchValue={search}
|
||||||
searchKey="message"
|
searchKey="message"
|
||||||
searchPlaceholder="Cari pesan log..."
|
searchPlaceholder="Cari pesan log..."
|
||||||
emptyText="Tidak ada entri log yang cocok."
|
emptyText="Tidak ada entri log yang cocok."
|
||||||
|
infiniteScroll={{ propName: 'entries' }}
|
||||||
toolbar={
|
toolbar={
|
||||||
<FilterDialog
|
<FilterDialog
|
||||||
fields={filterFields}
|
fields={filterFields}
|
||||||
|
|||||||
@ -91,16 +91,11 @@ export default function FeedbackIndex({
|
|||||||
total: feedbacks.total,
|
total: feedbacks.total,
|
||||||
};
|
};
|
||||||
|
|
||||||
const {
|
const { search, handleSearchChange, applyFilters } = useServerTable({
|
||||||
search,
|
|
||||||
handlePageChange,
|
|
||||||
handlePerPageChange,
|
|
||||||
handleSearchChange,
|
|
||||||
applyFilters,
|
|
||||||
} = useServerTable({
|
|
||||||
route: () => feedbackIndex.url(),
|
route: () => feedbackIndex.url(),
|
||||||
pagination,
|
pagination,
|
||||||
filters,
|
filters,
|
||||||
|
resetKeys: ['feedbacks'],
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleDelete() {
|
function handleDelete() {
|
||||||
@ -189,11 +184,10 @@ export default function FeedbackIndex({
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
data={feedbacks.data}
|
data={feedbacks.data}
|
||||||
pagination={pagination}
|
pagination={pagination}
|
||||||
onPageChange={handlePageChange}
|
|
||||||
onPerPageChange={handlePerPageChange}
|
|
||||||
onSearchChange={handleSearchChange}
|
onSearchChange={handleSearchChange}
|
||||||
searchValue={search}
|
searchValue={search}
|
||||||
searchKey="subject"
|
searchKey="subject"
|
||||||
|
infiniteScroll={{ propName: 'feedbacks' }}
|
||||||
toolbar={
|
toolbar={
|
||||||
<FilterDialog
|
<FilterDialog
|
||||||
fields={filterFields}
|
fields={filterFields}
|
||||||
|
|||||||
@ -185,16 +185,11 @@ export default function TuitionInvoiceIndex({
|
|||||||
total: invoices.total,
|
total: invoices.total,
|
||||||
};
|
};
|
||||||
|
|
||||||
const {
|
const { search, handleSearchChange, applyFilters } = useServerTable({
|
||||||
search,
|
|
||||||
handlePageChange,
|
|
||||||
handlePerPageChange,
|
|
||||||
handleSearchChange,
|
|
||||||
applyFilters,
|
|
||||||
} = useServerTable({
|
|
||||||
route: () => tuitionInvoiceIndex.url(),
|
route: () => tuitionInvoiceIndex.url(),
|
||||||
pagination,
|
pagination,
|
||||||
filters,
|
filters,
|
||||||
|
resetKeys: ['invoices'],
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleDelete() {
|
function handleDelete() {
|
||||||
@ -295,11 +290,10 @@ export default function TuitionInvoiceIndex({
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
data={invoices.data}
|
data={invoices.data}
|
||||||
pagination={pagination}
|
pagination={pagination}
|
||||||
onPageChange={handlePageChange}
|
|
||||||
onPerPageChange={handlePerPageChange}
|
|
||||||
onSearchChange={handleSearchChange}
|
onSearchChange={handleSearchChange}
|
||||||
searchValue={search}
|
searchValue={search}
|
||||||
searchKey="student"
|
searchKey="student"
|
||||||
|
infiniteScroll={{ propName: 'invoices' }}
|
||||||
toolbar={
|
toolbar={
|
||||||
<FilterDialog
|
<FilterDialog
|
||||||
fields={filterFields}
|
fields={filterFields}
|
||||||
@ -404,7 +398,8 @@ function CreateForm({
|
|||||||
!student.invoiced_term_ids?.includes(Number(academicTermId)),
|
!student.invoiced_term_ids?.includes(Number(academicTermId)),
|
||||||
)
|
)
|
||||||
: [];
|
: [];
|
||||||
const studentGroups = groupStudentsByDepartmentAndSemester(availableStudents);
|
const studentGroups =
|
||||||
|
groupStudentsByDepartmentAndSemester(availableStudents);
|
||||||
|
|
||||||
function reset() {
|
function reset() {
|
||||||
setAcademicTermId('');
|
setAcademicTermId('');
|
||||||
|
|||||||
@ -46,10 +46,7 @@ type Props = {
|
|||||||
highlight?: number;
|
highlight?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function AnnouncementIndex({
|
export default function AnnouncementIndex({ announcements, highlight }: Props) {
|
||||||
announcements,
|
|
||||||
highlight,
|
|
||||||
}: Props) {
|
|
||||||
const [createOpen, setCreateOpen] = useState(false);
|
const [createOpen, setCreateOpen] = useState(false);
|
||||||
const [editing, setEditing] = useState<Announcement | null>(null);
|
const [editing, setEditing] = useState<Announcement | null>(null);
|
||||||
const [deleting, setDeleting] = useState<Announcement | null>(null);
|
const [deleting, setDeleting] = useState<Announcement | null>(null);
|
||||||
@ -66,10 +63,10 @@ export default function AnnouncementIndex({
|
|||||||
total: announcements.total,
|
total: announcements.total,
|
||||||
};
|
};
|
||||||
|
|
||||||
const { search, handlePageChange, handlePerPageChange, handleSearchChange } =
|
const { search, handleSearchChange } = useServerTable({
|
||||||
useServerTable({
|
|
||||||
route: () => announcementIndex.url(),
|
route: () => announcementIndex.url(),
|
||||||
pagination,
|
pagination,
|
||||||
|
resetKeys: ['announcements'],
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleDelete() {
|
function handleDelete() {
|
||||||
@ -148,11 +145,10 @@ export default function AnnouncementIndex({
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
data={announcements.data}
|
data={announcements.data}
|
||||||
pagination={pagination}
|
pagination={pagination}
|
||||||
onPageChange={handlePageChange}
|
|
||||||
onPerPageChange={handlePerPageChange}
|
|
||||||
onSearchChange={handleSearchChange}
|
onSearchChange={handleSearchChange}
|
||||||
searchValue={search}
|
searchValue={search}
|
||||||
searchKey="title"
|
searchKey="title"
|
||||||
|
infiniteScroll={{ propName: 'announcements' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DeleteConfirmDialog
|
<DeleteConfirmDialog
|
||||||
@ -222,15 +218,11 @@ function ViewDetailDialog({
|
|||||||
<Label className="text-muted-foreground">
|
<Label className="text-muted-foreground">
|
||||||
Judul
|
Judul
|
||||||
</Label>
|
</Label>
|
||||||
<p className="font-medium">
|
<p className="font-medium">{announcement.title}</p>
|
||||||
{announcement.title}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-1">
|
<div className="grid gap-1">
|
||||||
<Label className="text-muted-foreground">
|
<Label className="text-muted-foreground">Isi</Label>
|
||||||
Isi
|
|
||||||
</Label>
|
|
||||||
<p className="whitespace-pre-line">
|
<p className="whitespace-pre-line">
|
||||||
{announcement.content}
|
{announcement.content}
|
||||||
</p>
|
</p>
|
||||||
@ -279,8 +271,7 @@ function AnnouncementFields({
|
|||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label>
|
<Label>
|
||||||
Ditujukan Untuk{' '}
|
Ditujukan Untuk <span className="text-destructive">*</span>
|
||||||
<span className="text-destructive">*</span>
|
|
||||||
</Label>
|
</Label>
|
||||||
<div className="flex flex-wrap gap-4">
|
<div className="flex flex-wrap gap-4">
|
||||||
{AnnouncementTargetRoles.map((role) => (
|
{AnnouncementTargetRoles.map((role) => (
|
||||||
@ -354,10 +345,7 @@ function EditForm({
|
|||||||
{({ errors }) =>
|
{({ errors }) =>
|
||||||
editing && (
|
editing && (
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
<AnnouncementFields
|
<AnnouncementFields errors={errors} editing={editing} />
|
||||||
errors={errors}
|
|
||||||
editing={editing}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -168,16 +168,11 @@ export default function CourseClassIndex({
|
|||||||
total: courseClasses.total,
|
total: courseClasses.total,
|
||||||
};
|
};
|
||||||
|
|
||||||
const {
|
const { search, handleSearchChange, applyFilters } = useServerTable({
|
||||||
search,
|
|
||||||
handlePageChange,
|
|
||||||
handlePerPageChange,
|
|
||||||
handleSearchChange,
|
|
||||||
applyFilters,
|
|
||||||
} = useServerTable({
|
|
||||||
route: () => courseClassIndex.url(),
|
route: () => courseClassIndex.url(),
|
||||||
pagination,
|
pagination,
|
||||||
filters,
|
filters,
|
||||||
|
resetKeys: ['courseClasses'],
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleDelete() {
|
function handleDelete() {
|
||||||
@ -286,10 +281,9 @@ export default function CourseClassIndex({
|
|||||||
data={courseClasses.data}
|
data={courseClasses.data}
|
||||||
searchKey="course"
|
searchKey="course"
|
||||||
pagination={pagination}
|
pagination={pagination}
|
||||||
onPageChange={handlePageChange}
|
|
||||||
onPerPageChange={handlePerPageChange}
|
|
||||||
onSearchChange={handleSearchChange}
|
onSearchChange={handleSearchChange}
|
||||||
searchValue={search}
|
searchValue={search}
|
||||||
|
infiniteScroll={{ propName: 'courseClasses' }}
|
||||||
groupBy={(courseClass) =>
|
groupBy={(courseClass) =>
|
||||||
`${courseClass.academic_term ? formatAcademicTermLabel(courseClass.academic_term) : 'Tanpa Periode'} — Semester ${courseClass.course?.semester_number ?? 'Tidak ditentukan'}`
|
`${courseClass.academic_term ? formatAcademicTermLabel(courseClass.academic_term) : 'Tanpa Periode'} — Semester ${courseClass.course?.semester_number ?? 'Tidak ditentukan'}`
|
||||||
}
|
}
|
||||||
@ -752,8 +746,7 @@ function EditForm({
|
|||||||
key={c.id}
|
key={c.id}
|
||||||
value={c}
|
value={c}
|
||||||
>
|
>
|
||||||
{c.code} -{' '}
|
{c.code} - {c.name}
|
||||||
{c.name}
|
|
||||||
</ComboboxItem>
|
</ComboboxItem>
|
||||||
)}
|
)}
|
||||||
</ComboboxCollection>
|
</ComboboxCollection>
|
||||||
@ -834,11 +827,10 @@ function DuplicateForm({
|
|||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Menyalin semua kelas mata kuliah (mata kuliah, dosen
|
Menyalin semua kelas mata kuliah (mata kuliah, dosen
|
||||||
pengampu, metode) dari periode sumber ke periode
|
pengampu, metode) dari periode sumber ke periode tujuan.
|
||||||
tujuan. Periode tujuan hanya bisa dipilih dari
|
Periode tujuan hanya bisa dipilih dari semester yang
|
||||||
semester yang sama (Ganjil ke Ganjil, Genap ke Genap).
|
sama (Ganjil ke Ganjil, Genap ke Genap). Mata kuliah
|
||||||
Mata kuliah yang sudah punya kelas di periode tujuan
|
yang sudah punya kelas di periode tujuan akan dilewati.
|
||||||
akan dilewati.
|
|
||||||
</p>
|
</p>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label>
|
<Label>
|
||||||
@ -871,9 +863,7 @@ function DuplicateForm({
|
|||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<InputError
|
<InputError message={errors.source_academic_term_id} />
|
||||||
message={errors.source_academic_term_id}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label>
|
<Label>
|
||||||
@ -910,9 +900,7 @@ function DuplicateForm({
|
|||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<InputError
|
<InputError message={errors.target_academic_term_id} />
|
||||||
message={errors.target_academic_term_id}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -49,16 +49,11 @@ export default function CourseRegistrationIndex({
|
|||||||
total: registrations.total,
|
total: registrations.total,
|
||||||
};
|
};
|
||||||
|
|
||||||
const {
|
const { search, handleSearchChange, applyFilters } = useServerTable({
|
||||||
search,
|
|
||||||
handlePageChange,
|
|
||||||
handlePerPageChange,
|
|
||||||
handleSearchChange,
|
|
||||||
applyFilters,
|
|
||||||
} = useServerTable({
|
|
||||||
route: () => index.url(),
|
route: () => index.url(),
|
||||||
pagination,
|
pagination,
|
||||||
filters,
|
filters,
|
||||||
|
resetKeys: ['registrations'],
|
||||||
});
|
});
|
||||||
|
|
||||||
const columns = createCourseRegistrationColumns();
|
const columns = createCourseRegistrationColumns();
|
||||||
@ -74,11 +69,10 @@ export default function CourseRegistrationIndex({
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
data={registrations.data}
|
data={registrations.data}
|
||||||
pagination={pagination}
|
pagination={pagination}
|
||||||
onPageChange={handlePageChange}
|
|
||||||
onPerPageChange={handlePerPageChange}
|
|
||||||
onSearchChange={handleSearchChange}
|
onSearchChange={handleSearchChange}
|
||||||
searchValue={search}
|
searchValue={search}
|
||||||
searchKey="student"
|
searchKey="student"
|
||||||
|
infiniteScroll={{ propName: 'registrations' }}
|
||||||
groupBy={(row) =>
|
groupBy={(row) =>
|
||||||
row.student?.department?.name ?? 'Tanpa Jurusan'
|
row.student?.department?.name ?? 'Tanpa Jurusan'
|
||||||
}
|
}
|
||||||
|
|||||||
@ -83,16 +83,11 @@ export default function AcademicTermIndex({
|
|||||||
total: academicTerms.total,
|
total: academicTerms.total,
|
||||||
};
|
};
|
||||||
|
|
||||||
const {
|
const { search, handleSearchChange, applyFilters } = useServerTable({
|
||||||
search,
|
|
||||||
handlePageChange,
|
|
||||||
handlePerPageChange,
|
|
||||||
handleSearchChange,
|
|
||||||
applyFilters,
|
|
||||||
} = useServerTable({
|
|
||||||
route: () => academicTermIndex.url(),
|
route: () => academicTermIndex.url(),
|
||||||
pagination,
|
pagination,
|
||||||
filters,
|
filters,
|
||||||
|
resetKeys: ['academicTerms'],
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleDelete() {
|
function handleDelete() {
|
||||||
@ -184,10 +179,9 @@ export default function AcademicTermIndex({
|
|||||||
data={academicTerms.data}
|
data={academicTerms.data}
|
||||||
searchKey="academic_year"
|
searchKey="academic_year"
|
||||||
pagination={pagination}
|
pagination={pagination}
|
||||||
onPageChange={handlePageChange}
|
|
||||||
onPerPageChange={handlePerPageChange}
|
|
||||||
onSearchChange={handleSearchChange}
|
onSearchChange={handleSearchChange}
|
||||||
searchValue={search}
|
searchValue={search}
|
||||||
|
infiniteScroll={{ propName: 'academicTerms' }}
|
||||||
toolbar={
|
toolbar={
|
||||||
<FilterDialog
|
<FilterDialog
|
||||||
fields={filterFields}
|
fields={filterFields}
|
||||||
|
|||||||
@ -91,16 +91,11 @@ export default function CourseIndex({
|
|||||||
total: courses.total,
|
total: courses.total,
|
||||||
};
|
};
|
||||||
|
|
||||||
const {
|
const { search, handleSearchChange, applyFilters } = useServerTable({
|
||||||
search,
|
|
||||||
handlePageChange,
|
|
||||||
handlePerPageChange,
|
|
||||||
handleSearchChange,
|
|
||||||
applyFilters,
|
|
||||||
} = useServerTable({
|
|
||||||
route: () => courseIndex.url(),
|
route: () => courseIndex.url(),
|
||||||
pagination,
|
pagination,
|
||||||
filters,
|
filters,
|
||||||
|
resetKeys: ['courses'],
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleDelete() {
|
function handleDelete() {
|
||||||
@ -187,10 +182,9 @@ export default function CourseIndex({
|
|||||||
data={courses.data}
|
data={courses.data}
|
||||||
searchKey="name"
|
searchKey="name"
|
||||||
pagination={pagination}
|
pagination={pagination}
|
||||||
onPageChange={handlePageChange}
|
|
||||||
onPerPageChange={handlePerPageChange}
|
|
||||||
onSearchChange={handleSearchChange}
|
onSearchChange={handleSearchChange}
|
||||||
searchValue={search}
|
searchValue={search}
|
||||||
|
infiniteScroll={{ propName: 'courses' }}
|
||||||
groupBy={(course) =>
|
groupBy={(course) =>
|
||||||
`${course.department?.name ?? 'Tanpa Jurusan'} - Semester ${course.semester_number ?? 'Tidak ditentukan'}`
|
`${course.department?.name ?? 'Tanpa Jurusan'} - Semester ${course.semester_number ?? 'Tidak ditentukan'}`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -84,14 +84,10 @@ export default function DepartmentIndex({
|
|||||||
total: departments.total,
|
total: departments.total,
|
||||||
};
|
};
|
||||||
|
|
||||||
const {
|
const { search, handleSearchChange } = useServerTable({
|
||||||
search,
|
|
||||||
handlePageChange,
|
|
||||||
handlePerPageChange,
|
|
||||||
handleSearchChange,
|
|
||||||
} = useServerTable({
|
|
||||||
route: () => departmentIndex.url(),
|
route: () => departmentIndex.url(),
|
||||||
pagination,
|
pagination,
|
||||||
|
resetKeys: ['departments'],
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleDelete() {
|
function handleDelete() {
|
||||||
@ -179,10 +175,9 @@ export default function DepartmentIndex({
|
|||||||
data={departments.data}
|
data={departments.data}
|
||||||
searchKey="name"
|
searchKey="name"
|
||||||
pagination={pagination}
|
pagination={pagination}
|
||||||
onPageChange={handlePageChange}
|
|
||||||
onPerPageChange={handlePerPageChange}
|
|
||||||
onSearchChange={handleSearchChange}
|
onSearchChange={handleSearchChange}
|
||||||
searchValue={search}
|
searchValue={search}
|
||||||
|
infiniteScroll={{ propName: 'departments' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DeleteConfirmDialog
|
<DeleteConfirmDialog
|
||||||
@ -245,8 +240,8 @@ function LeadershipHistoryDialog({
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">
|
<p className="font-medium">
|
||||||
{entry.lecturer?.user?.profile
|
{entry.lecturer?.user?.profile?.full_name ??
|
||||||
?.full_name ?? 'N/A'}
|
'N/A'}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
{format(
|
{format(
|
||||||
@ -300,9 +295,7 @@ function KaprodiFields({
|
|||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label>
|
<Label>
|
||||||
Kaprodi{' '}
|
Kaprodi{' '}
|
||||||
{editing && (
|
{editing && <span className="text-destructive">*</span>}
|
||||||
<span className="text-destructive">*</span>
|
|
||||||
)}
|
|
||||||
</Label>
|
</Label>
|
||||||
<input
|
<input
|
||||||
type="hidden"
|
type="hidden"
|
||||||
@ -358,9 +351,7 @@ function KaprodiFields({
|
|||||||
<input
|
<input
|
||||||
type="hidden"
|
type="hidden"
|
||||||
name="leadership_started_at"
|
name="leadership_started_at"
|
||||||
value={
|
value={startedAt ? format(startedAt, 'yyyy-MM-dd') : ''}
|
||||||
startedAt ? format(startedAt, 'yyyy-MM-dd') : ''
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<DatePicker value={startedAt} onChange={setStartedAt} />
|
<DatePicker value={startedAt} onChange={setStartedAt} />
|
||||||
<InputError message={errors.leadership_started_at} />
|
<InputError message={errors.leadership_started_at} />
|
||||||
|
|||||||
@ -105,16 +105,11 @@ export default function AcademicAdvisingLogIndex({
|
|||||||
total: logs.total,
|
total: logs.total,
|
||||||
};
|
};
|
||||||
|
|
||||||
const {
|
const { search, handleSearchChange, applyFilters } = useServerTable({
|
||||||
search,
|
|
||||||
handlePageChange,
|
|
||||||
handlePerPageChange,
|
|
||||||
handleSearchChange,
|
|
||||||
applyFilters,
|
|
||||||
} = useServerTable({
|
|
||||||
route: () => academicAdvisingLogIndex.url(),
|
route: () => academicAdvisingLogIndex.url(),
|
||||||
pagination,
|
pagination,
|
||||||
filters,
|
filters,
|
||||||
|
resetKeys: ['logs'],
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleDelete() {
|
function handleDelete() {
|
||||||
@ -198,11 +193,10 @@ export default function AcademicAdvisingLogIndex({
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
data={logs.data}
|
data={logs.data}
|
||||||
pagination={pagination}
|
pagination={pagination}
|
||||||
onPageChange={handlePageChange}
|
|
||||||
onPerPageChange={handlePerPageChange}
|
|
||||||
onSearchChange={handleSearchChange}
|
onSearchChange={handleSearchChange}
|
||||||
searchValue={search}
|
searchValue={search}
|
||||||
searchKey="student"
|
searchKey="student"
|
||||||
|
infiniteScroll={{ propName: 'logs' }}
|
||||||
toolbar={
|
toolbar={
|
||||||
<FilterDialog
|
<FilterDialog
|
||||||
fields={filterFields}
|
fields={filterFields}
|
||||||
@ -277,8 +271,8 @@ function ViewDetailDialog({
|
|||||||
<ul className="grid gap-1">
|
<ul className="grid gap-1">
|
||||||
{log.students.map((student) => (
|
{log.students.map((student) => (
|
||||||
<li key={student.id}>
|
<li key={student.id}>
|
||||||
{student.user?.profile
|
{student.user?.profile?.full_name ??
|
||||||
?.full_name ?? 'N/A'}{' '}
|
'N/A'}{' '}
|
||||||
· {student.student_number}
|
· {student.student_number}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
@ -338,8 +332,7 @@ function AdvisingLogFields({
|
|||||||
<>
|
<>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label>
|
<Label>
|
||||||
Dosen{' '}
|
Dosen <span className="text-destructive">*</span>
|
||||||
<span className="text-destructive">*</span>
|
|
||||||
</Label>
|
</Label>
|
||||||
<input
|
<input
|
||||||
type="hidden"
|
type="hidden"
|
||||||
@ -391,8 +384,7 @@ function AdvisingLogFields({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{selectedStudents.length ===
|
{selectedStudents.length === availableStudents.length &&
|
||||||
availableStudents.length &&
|
|
||||||
availableStudents.length > 0
|
availableStudents.length > 0
|
||||||
? 'Batalkan Semua'
|
? 'Batalkan Semua'
|
||||||
: 'Pilih Semua'}
|
: 'Pilih Semua'}
|
||||||
|
|||||||
@ -75,16 +75,11 @@ export default function LetterRequestIndex({
|
|||||||
total: letterRequests.total,
|
total: letterRequests.total,
|
||||||
};
|
};
|
||||||
|
|
||||||
const {
|
const { search, handleSearchChange, applyFilters } = useServerTable({
|
||||||
search,
|
|
||||||
handlePageChange,
|
|
||||||
handlePerPageChange,
|
|
||||||
handleSearchChange,
|
|
||||||
applyFilters,
|
|
||||||
} = useServerTable({
|
|
||||||
route: () => letterRequestIndex.url(),
|
route: () => letterRequestIndex.url(),
|
||||||
pagination,
|
pagination,
|
||||||
filters,
|
filters,
|
||||||
|
resetKeys: ['letterRequests'],
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleDelete() {
|
function handleDelete() {
|
||||||
@ -189,11 +184,10 @@ export default function LetterRequestIndex({
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
data={letterRequests.data}
|
data={letterRequests.data}
|
||||||
pagination={pagination}
|
pagination={pagination}
|
||||||
onPageChange={handlePageChange}
|
|
||||||
onPerPageChange={handlePerPageChange}
|
|
||||||
onSearchChange={handleSearchChange}
|
onSearchChange={handleSearchChange}
|
||||||
searchValue={search}
|
searchValue={search}
|
||||||
searchKey="student"
|
searchKey="student"
|
||||||
|
infiniteScroll={{ propName: 'letterRequests' }}
|
||||||
toolbar={
|
toolbar={
|
||||||
<FilterDialog
|
<FilterDialog
|
||||||
fields={filterFields}
|
fields={filterFields}
|
||||||
@ -343,7 +337,10 @@ function EditForm({
|
|||||||
{({ errors }) =>
|
{({ errors }) =>
|
||||||
editing && (
|
editing && (
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
<LetterRequestFields errors={errors} editing={editing} />
|
<LetterRequestFields
|
||||||
|
errors={errors}
|
||||||
|
editing={editing}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -59,16 +59,11 @@ export default function AdministratorIndex({ administrators, filters }: Props) {
|
|||||||
total: administrators.total,
|
total: administrators.total,
|
||||||
};
|
};
|
||||||
|
|
||||||
const {
|
const { search, handleSearchChange, applyFilters } = useServerTable({
|
||||||
search,
|
|
||||||
handlePageChange,
|
|
||||||
handlePerPageChange,
|
|
||||||
handleSearchChange,
|
|
||||||
applyFilters,
|
|
||||||
} = useServerTable({
|
|
||||||
route: () => administratorsIndex.url(),
|
route: () => administratorsIndex.url(),
|
||||||
pagination,
|
pagination,
|
||||||
filters,
|
filters,
|
||||||
|
resetKeys: ['administrators'],
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleDelete() {
|
function handleDelete() {
|
||||||
@ -140,10 +135,9 @@ export default function AdministratorIndex({ administrators, filters }: Props) {
|
|||||||
data={administrators.data}
|
data={administrators.data}
|
||||||
searchKey="name"
|
searchKey="name"
|
||||||
pagination={pagination}
|
pagination={pagination}
|
||||||
onPageChange={handlePageChange}
|
|
||||||
onPerPageChange={handlePerPageChange}
|
|
||||||
onSearchChange={handleSearchChange}
|
onSearchChange={handleSearchChange}
|
||||||
searchValue={search}
|
searchValue={search}
|
||||||
|
infiniteScroll={{ propName: 'administrators' }}
|
||||||
toolbar={
|
toolbar={
|
||||||
<FilterDialog
|
<FilterDialog
|
||||||
fields={filterFields}
|
fields={filterFields}
|
||||||
|
|||||||
@ -82,16 +82,11 @@ export default function LecturerIndex({
|
|||||||
total: lecturers.total,
|
total: lecturers.total,
|
||||||
};
|
};
|
||||||
|
|
||||||
const {
|
const { search, handleSearchChange, applyFilters } = useServerTable({
|
||||||
search,
|
|
||||||
handlePageChange,
|
|
||||||
handlePerPageChange,
|
|
||||||
handleSearchChange,
|
|
||||||
applyFilters,
|
|
||||||
} = useServerTable({
|
|
||||||
route: () => lecturersIndex.url(),
|
route: () => lecturersIndex.url(),
|
||||||
pagination,
|
pagination,
|
||||||
filters,
|
filters,
|
||||||
|
resetKeys: ['lecturers'],
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleDelete() {
|
function handleDelete() {
|
||||||
@ -177,10 +172,9 @@ export default function LecturerIndex({
|
|||||||
data={lecturers.data}
|
data={lecturers.data}
|
||||||
searchKey="name"
|
searchKey="name"
|
||||||
pagination={pagination}
|
pagination={pagination}
|
||||||
onPageChange={handlePageChange}
|
|
||||||
onPerPageChange={handlePerPageChange}
|
|
||||||
onSearchChange={handleSearchChange}
|
onSearchChange={handleSearchChange}
|
||||||
searchValue={search}
|
searchValue={search}
|
||||||
|
infiniteScroll={{ propName: 'lecturers' }}
|
||||||
toolbar={
|
toolbar={
|
||||||
<FilterDialog
|
<FilterDialog
|
||||||
fields={filterFields}
|
fields={filterFields}
|
||||||
|
|||||||
@ -109,16 +109,11 @@ export default function StudentIndex({
|
|||||||
total: students.total,
|
total: students.total,
|
||||||
};
|
};
|
||||||
|
|
||||||
const {
|
const { search, handleSearchChange, applyFilters } = useServerTable({
|
||||||
search,
|
|
||||||
handlePageChange,
|
|
||||||
handlePerPageChange,
|
|
||||||
handleSearchChange,
|
|
||||||
applyFilters,
|
|
||||||
} = useServerTable({
|
|
||||||
route: () => studentsIndex.url(),
|
route: () => studentsIndex.url(),
|
||||||
pagination,
|
pagination,
|
||||||
filters,
|
filters,
|
||||||
|
resetKeys: ['students'],
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleDelete() {
|
function handleDelete() {
|
||||||
@ -226,10 +221,9 @@ export default function StudentIndex({
|
|||||||
data={students.data}
|
data={students.data}
|
||||||
searchKey="name"
|
searchKey="name"
|
||||||
pagination={pagination}
|
pagination={pagination}
|
||||||
onPageChange={handlePageChange}
|
|
||||||
onPerPageChange={handlePerPageChange}
|
|
||||||
onSearchChange={handleSearchChange}
|
onSearchChange={handleSearchChange}
|
||||||
searchValue={search}
|
searchValue={search}
|
||||||
|
infiniteScroll={{ propName: 'students' }}
|
||||||
toolbar={
|
toolbar={
|
||||||
<FilterDialog
|
<FilterDialog
|
||||||
fields={filterFields}
|
fields={filterFields}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user