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