feat: enhance course pagination and grouping by department and semester
This commit is contained in:
parent
9823b879d8
commit
86863ff2f8
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user