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:
Yoga Pangestu 2026-09-04 13:59:26 +07:00
parent 33f354ad94
commit 35a64c2045
32 changed files with 374 additions and 373 deletions

View File

@ -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']),
]); ]);

View File

@ -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,

View 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']),

View File

@ -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,

View File

@ -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(),
), )),
]); ]);
} }

View File

@ -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(),

View File

@ -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']),
]); ]);
} }

View File

@ -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']),
]); ]);
} }

View File

@ -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']),

View File

@ -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(),
]); ]);
} }

View File

@ -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']),

View File

@ -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']),
]); ]);
} }

View File

@ -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']),
]); ]);
} }

View File

@ -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']),
]); ]);

View File

@ -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(),

View File

@ -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 ? (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"> isInfiniteScroll ? (
{data.map((item, index) => ( <InfiniteScroll
<Card data={infiniteScroll.propName}
key={getRowId ? getRowId(item) : index} as="div"
className="overflow-hidden" onlyNext
> preserveUrl
<CardContent>{renderCard(item, index)}</CardContent> buffer={infiniteScroll.buffer ?? 300}
</Card> className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
))} next={({ loading, hasMore }) =>
</div> 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">
{data.map((item, index) => (
<Card
key={getRowId ? getRowId(item) : index}
className="overflow-hidden"
>
<CardContent>
{renderCard(item, index)}
</CardContent>
</Card>
))}
</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,45 +177,52 @@ export function DataCards<TData>({
</Card> </Card>
)} )}
<div className="flex items-center justify-end gap-2"> {isInfiniteScroll ? (
<span className="text-sm text-muted-foreground"> <p className="text-center text-sm text-muted-foreground">
{`Halaman ${currentPage} dari ${totalPages}`} Menampilkan {data.length.toLocaleString('id-ID')} dari{' '}
</span> {pagination.total.toLocaleString('id-ID')} data
<div className="flex items-center gap-1"> </p>
<Button ) : (
variant="outline" <div className="flex items-center justify-end gap-2">
size="sm" <span className="text-sm text-muted-foreground">
onClick={() => onPageChange(1)} {`Halaman ${currentPage} dari ${totalPages}`}
disabled={currentPage <= 1} </span>
> <div className="flex items-center gap-1">
<ChevronsLeft className="h-4 w-4" /> <Button
</Button> variant="outline"
<Button size="sm"
variant="outline" onClick={() => onPageChange?.(1)}
size="sm" disabled={currentPage <= 1}
onClick={() => onPageChange(currentPage - 1)} >
disabled={currentPage <= 1} <ChevronsLeft className="h-4 w-4" />
> </Button>
<ChevronLeft className="h-4 w-4" /> <Button
</Button> variant="outline"
<Button size="sm"
variant="outline" onClick={() => onPageChange?.(currentPage - 1)}
size="sm" disabled={currentPage <= 1}
onClick={() => onPageChange(currentPage + 1)} >
disabled={currentPage >= totalPages} <ChevronLeft className="h-4 w-4" />
> </Button>
<ChevronRight className="h-4 w-4" /> <Button
</Button> variant="outline"
<Button size="sm"
variant="outline" onClick={() => onPageChange?.(currentPage + 1)}
size="sm" disabled={currentPage >= totalPages}
onClick={() => onPageChange(totalPages)} >
disabled={currentPage >= totalPages} <ChevronRight className="h-4 w-4" />
> </Button>
<ChevronsRight className="h-4 w-4" /> <Button
</Button> variant="outline"
size="sm"
onClick={() => onPageChange?.(totalPages)}
disabled={currentPage >= totalPages}
>
<ChevronsRight className="h-4 w-4" />
</Button>
</div>
</div> </div>
</div> )}
</> </>
); );
} }

View File

@ -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,34 +459,32 @@ export function DataTable<TData, TValue>({
> >
{row {row
.getVisibleCells() .getVisibleCells()
.map( .map((cell) => (
(cell) => ( <TableCell
<TableCell key={
key={ cell.id
cell.id }
} className={
className={ (
(
cell
.column
.columnDef
.meta as {
className?: string;
}
)
?.className
}
>
{flexRender(
cell cell
.column .column
.columnDef .columnDef
.cell, .meta as {
cell.getContext(), className?: string;
)} }
</TableCell> )
), ?.className
)} }
>
{flexRender(
cell
.column
.columnDef
.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow> </TableRow>
{renderSubRow && {renderSubRow &&
row.getIsExpanded() && ( row.getIsExpanded() && (
@ -512,88 +524,124 @@ export function DataTable<TData, TValue>({
</CardContent> </CardContent>
</Card> </Card>
<div className="flex items-center justify-end gap-2"> {isInfiniteScroll ? (
<span className="text-sm text-muted-foreground"> <div className="flex flex-col items-center gap-1">
{isServerMode <span className="text-sm text-muted-foreground">
? `Halaman ${currentPage} dari ${totalPages}` Menampilkan {data.length.toLocaleString('id-ID')} dari{' '}
: `Halaman ${table.getState().pagination.pageIndex + 1} dari ${table.getPageCount()}`} {(pagination?.total ?? data.length).toLocaleString(
</span> 'id-ID',
<div className="flex items-center gap-1"> )}{' '}
{isServerMode ? ( data
<> </span>
<Button <InfiniteScroll
variant="outline" data={infiniteScroll.propName}
size="sm" itemsElement={tbodyRef}
onClick={() => onPageChange(1)} onlyNext
disabled={currentPage <= 1} preserveUrl
> buffer={infiniteScroll.buffer ?? 300}
<ChevronsLeft className="h-4 w-4" /> next={({ loading, hasMore }) =>
</Button> loading ? (
<Button <span className="block py-2 text-center text-sm text-muted-foreground">
variant="outline" Memuat data...
size="sm" </span>
onClick={() => onPageChange(currentPage - 1)} ) : hasMore ? null : (
disabled={currentPage <= 1} <span className="block py-2 text-center text-sm text-muted-foreground">
> Semua data telah ditampilkan.
<ChevronLeft className="h-4 w-4" /> </span>
</Button> )
<Button }
variant="outline" />
size="sm"
onClick={() => onPageChange(currentPage + 1)}
disabled={currentPage >= totalPages}
>
<ChevronRight className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => onPageChange(totalPages)}
disabled={currentPage >= totalPages}
>
<ChevronsRight className="h-4 w-4" />
</Button>
</>
) : (
<>
<Button
variant="outline"
size="sm"
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
<ChevronsLeft className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<ChevronLeft className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<ChevronRight className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() =>
table.setPageIndex(table.getPageCount() - 1)
}
disabled={!table.getCanNextPage()}
>
<ChevronsRight className="h-4 w-4" />
</Button>
</>
)}
</div> </div>
</div> ) : (
<div className="flex items-center justify-end gap-2">
<span className="text-sm text-muted-foreground">
{hasServerPager
? `Halaman ${currentPage} dari ${totalPages}`
: `Halaman ${table.getState().pagination.pageIndex + 1} dari ${table.getPageCount()}`}
</span>
<div className="flex items-center gap-1">
{hasServerPager ? (
<>
<Button
variant="outline"
size="sm"
onClick={() => onPageChange(1)}
disabled={currentPage <= 1}
>
<ChevronsLeft className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() =>
onPageChange(currentPage - 1)
}
disabled={currentPage <= 1}
>
<ChevronLeft className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() =>
onPageChange(currentPage + 1)
}
disabled={currentPage >= totalPages}
>
<ChevronRight className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => onPageChange(totalPages)}
disabled={currentPage >= totalPages}
>
<ChevronsRight className="h-4 w-4" />
</Button>
</>
) : (
<>
<Button
variant="outline"
size="sm"
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
<ChevronsLeft className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<ChevronLeft className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<ChevronRight className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() =>
table.setPageIndex(
table.getPageCount() - 1,
)
}
disabled={!table.getCanNextPage()}
>
<ChevronsRight className="h-4 w-4" />
</Button>
</>
)}
</div>
</div>
)}
</> </>
); );
} }

View File

@ -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}
/> />
)} )}

View File

@ -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}

View File

@ -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}

View File

@ -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('');

View File

@ -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,11 +63,11 @@ 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() {
if (!deleting) { if (!deleting) {
@ -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>
) )
} }

View File

@ -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>
)} )}

View File

@ -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'
} }

View File

@ -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}

View File

@ -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'}`
} }

View File

@ -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} />

View File

@ -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'}{' '}
&middot; {student.student_number} &middot; {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'}

View File

@ -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>
) )
} }

View File

@ -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}

View File

@ -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}

View File

@ -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}