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();
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')
->when($search, fn ($q) => $q->where('name', 'like', "%{$search}%")->orWhere('code', 'like', "%{$search}%"))
->when($departmentId, fn ($q) => $q->where('department_id', $departmentId))
->when($semesterNumber, fn ($q) => $q->where('semester_number', $semesterNumber))
->when($user->hasRole('mahasiswa'), fn ($q) => $q->where('department_id', $user->student?->department_id))
->when($user->hasRole('dosen'), fn ($q) => $q->whereIn('department_id', $user->lecturer?->departments()->pluck('departments.id') ?? []))
->latest()
->when($search, fn ($q) => $q->where('courses.name', 'like', "%{$search}%")->orWhere('courses.code', 'like', "%{$search}%"))
->when($departmentId, fn ($q) => $q->where('courses.department_id', $departmentId))
->when($semesterNumber, fn ($q) => $q->where('courses.semester_number', $semesterNumber))
->when($user->hasRole('mahasiswa'), fn ($q) => $q->where('courses.department_id', $user->student?->department_id))
->when($user->hasRole('dosen'), fn ($q) => $q->whereIn('courses.department_id', $user->lecturer?->departments()->pluck('departments.id') ?? []))
->orderBy('departments.name')
->orderBy('courses.semester_number')
->paginate($perPage);
}

View File

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

View File

@ -1,7 +1,6 @@
import type { ColumnDef } from '@tanstack/react-table';
import { Pencil, Trash2 } from 'lucide-react';
import { RowActions } from '@/components/row-actions';
import { Badge } from '@/components/ui/badge';
import type { Course } from '@/types/course';
export type { Course } from '@/types/course';
@ -33,11 +32,6 @@ export function createCourseColumns(
header: () => <span>Nama Mata Kuliah</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',
header: () => <span className="block text-center">SKS</span>,
@ -51,28 +45,6 @@ export function createCourseColumns(
</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) {

View File

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