feat: enhance course pagination and grouping by department and semester

This commit is contained in:
Yoga Pangestu 2026-09-02 13:38:40 +07:00
parent 9823b879d8
commit 86863ff2f8
4 changed files with 100 additions and 83 deletions

View File

@ -29,14 +29,16 @@ public function paginated(int $perPage = 25, string $search = '', ?int $departme
$user = auth()->user(); $user = auth()->user();
return Course::query() return Course::query()
->select(['id', 'code', 'name', 'credits', 'department_id', 'semester_number']) ->select(['courses.id', 'courses.code', 'courses.name', 'courses.credits', 'courses.department_id', 'courses.semester_number'])
->join('departments', 'departments.id', '=', 'courses.department_id')
->with('department:id,name') ->with('department:id,name')
->when($search, fn ($q) => $q->where('name', 'like', "%{$search}%")->orWhere('code', 'like', "%{$search}%")) ->when($search, fn ($q) => $q->where('courses.name', 'like', "%{$search}%")->orWhere('courses.code', 'like', "%{$search}%"))
->when($departmentId, fn ($q) => $q->where('department_id', $departmentId)) ->when($departmentId, fn ($q) => $q->where('courses.department_id', $departmentId))
->when($semesterNumber, fn ($q) => $q->where('semester_number', $semesterNumber)) ->when($semesterNumber, fn ($q) => $q->where('courses.semester_number', $semesterNumber))
->when($user->hasRole('mahasiswa'), fn ($q) => $q->where('department_id', $user->student?->department_id)) ->when($user->hasRole('mahasiswa'), fn ($q) => $q->where('courses.department_id', $user->student?->department_id))
->when($user->hasRole('dosen'), fn ($q) => $q->whereIn('department_id', $user->lecturer?->departments()->pluck('departments.id') ?? [])) ->when($user->hasRole('dosen'), fn ($q) => $q->whereIn('courses.department_id', $user->lecturer?->departments()->pluck('departments.id') ?? []))
->latest() ->orderBy('departments.name')
->orderBy('courses.semester_number')
->paginate($perPage); ->paginate($perPage);
} }

View File

@ -65,6 +65,7 @@ interface DataTableProps<TData, TValue> {
toolbar?: React.ReactNode; toolbar?: React.ReactNode;
renderSubRow?: (row: Row<TData>, searchValue?: string) => React.ReactNode; renderSubRow?: (row: Row<TData>, searchValue?: string) => React.ReactNode;
defaultExpanded?: boolean; defaultExpanded?: boolean;
groupBy?: (item: TData) => string;
pagination?: PaginationState; pagination?: PaginationState;
onPageChange?: (page: number) => void; onPageChange?: (page: number) => void;
onPerPageChange?: (perPage: number) => void; onPerPageChange?: (perPage: number) => void;
@ -156,6 +157,7 @@ export function DataTable<TData, TValue>({
toolbar, toolbar,
renderSubRow, renderSubRow,
defaultExpanded = false, defaultExpanded = false,
groupBy,
pagination, pagination,
onPageChange, onPageChange,
onPerPageChange, onPerPageChange,
@ -404,58 +406,96 @@ export function DataTable<TData, TValue>({
</SortableContext> </SortableContext>
</DndContext> </DndContext>
) : ( ) : (
table.getRowModel().rows.map((row) => ( (() => {
<React.Fragment key={row.id}> let previousGroup: string | null =
<TableRow null;
data-state={
row.getIsSelected() && return table
'selected' .getRowModel()
} .rows.map((row) => {
> const group = groupBy
{row ? groupBy(row.original)
.getVisibleCells() : null;
.map((cell) => ( const showGroupHeader =
<TableCell groupBy &&
key={cell.id} group !== previousGroup;
className={ previousGroup = group;
(
cell.column return (
.columnDef <React.Fragment
.meta as { key={row.id}
className?: string; >
{showGroupHeader && (
<TableRow className="bg-muted/50 hover:bg-muted/50">
<TableCell
colSpan={
visibleColumns.length
} }
)?.className className="py-2 text-sm font-semibold"
>
{group}
</TableCell>
</TableRow>
)}
<TableRow
data-state={
row.getIsSelected() &&
'selected'
} }
> >
{flexRender( {row
cell.column .getVisibleCells()
.columnDef .map(
.cell, (cell) => (
cell.getContext(), <TableCell
)} key={
</TableCell> cell.id
))} }
</TableRow> className={
{renderSubRow && (
row.getIsExpanded() && ( cell
<TableRow> .column
<TableCell .columnDef
colSpan={ .meta as {
visibleColumns.length className?: string;
} }
className="bg-muted/50 p-0" )
> ?.className
<div className="p-4"> }
{renderSubRow( >
row, {flexRender(
localSearch, cell
.column
.columnDef
.cell,
cell.getContext(),
)}
</TableCell>
),
)} )}
</div> </TableRow>
</TableCell> {renderSubRow &&
</TableRow> row.getIsExpanded() && (
)} <TableRow>
</React.Fragment> <TableCell
)) colSpan={
visibleColumns.length
}
className="bg-muted/50 p-0"
>
<div className="p-4">
{renderSubRow(
row,
localSearch,
)}
</div>
</TableCell>
</TableRow>
)}
</React.Fragment>
);
});
})()
) )
) : ( ) : (
<TableRow> <TableRow>

View File

@ -1,7 +1,6 @@
import type { ColumnDef } from '@tanstack/react-table'; import type { ColumnDef } from '@tanstack/react-table';
import { Pencil, Trash2 } from 'lucide-react'; import { Pencil, Trash2 } from 'lucide-react';
import { RowActions } from '@/components/row-actions'; import { RowActions } from '@/components/row-actions';
import { Badge } from '@/components/ui/badge';
import type { Course } from '@/types/course'; import type { Course } from '@/types/course';
export type { Course } from '@/types/course'; export type { Course } from '@/types/course';
@ -33,11 +32,6 @@ export function createCourseColumns(
header: () => <span>Nama Mata Kuliah</span>, header: () => <span>Nama Mata Kuliah</span>,
cell: ({ row }) => <span>{row.getValue('name') as string}</span>, cell: ({ row }) => <span>{row.getValue('name') as string}</span>,
}, },
{
accessorKey: 'department.name',
header: () => <span>Jurusan</span>,
cell: ({ row }) => row.original.department?.name ?? '-',
},
{ {
accessorKey: 'credits', accessorKey: 'credits',
header: () => <span className="block text-center">SKS</span>, header: () => <span className="block text-center">SKS</span>,
@ -51,28 +45,6 @@ export function createCourseColumns(
</div> </div>
), ),
}, },
{
accessorKey: 'semester_number',
header: () => <span className="block text-center">Semester</span>,
meta: {
className: 'w-[100px] text-center',
headerClassName: 'w-[100px] text-center',
},
cell: ({ row }) => {
const semesterNumber = row.getValue('semester_number') as
number | null;
return (
<div className="flex justify-center">
{semesterNumber ? (
<Badge variant="secondary">{semesterNumber}</Badge>
) : (
<span className="text-muted-foreground">-</span>
)}
</div>
);
},
},
]; ];
if (canUpdate || canDelete) { if (canUpdate || canDelete) {

View File

@ -191,6 +191,9 @@ export default function CourseIndex({
onPerPageChange={handlePerPageChange} onPerPageChange={handlePerPageChange}
onSearchChange={handleSearchChange} onSearchChange={handleSearchChange}
searchValue={search} searchValue={search}
groupBy={(course) =>
`${course.department?.name ?? 'Tanpa Jurusan'} - Semester ${course.semester_number ?? 'Tidak ditentukan'}`
}
toolbar={ toolbar={
<FilterDialog <FilterDialog
fields={filterFields} fields={filterFields}