feat: enhance assignment and material management; add academic term filtering and improve pagination handling
This commit is contained in:
parent
892d14c13a
commit
b99876fb4c
@ -23,15 +23,27 @@ public function __construct(
|
||||
|
||||
public function index(PaginatedRequest $request): Response
|
||||
{
|
||||
$academicTermSelected = $request->has('academic_term_id');
|
||||
$academicTermId = $academicTermSelected
|
||||
? ((int) $request->validated('academic_term_id') ?: null)
|
||||
: $this->academicTermService->getActive()?->id;
|
||||
|
||||
return Inertia::render('admin/academic-classes/assignments/index', [
|
||||
'assignments' => Inertia::scroll(fn () => $this->service->paginated(
|
||||
$request->user(),
|
||||
...$request->validatedWithDefaults(),
|
||||
courseClassId: $request->validated('course_class_id'),
|
||||
academicTermId: $this->academicTermService->getActive()?->id,
|
||||
academicTermId: $academicTermId,
|
||||
)),
|
||||
'courseClasses' => $this->courseClassService->getAllForSelect($request->user()),
|
||||
'filters' => $request->only(['course_class_id']),
|
||||
'filterCourseClasses' => Inertia::always($this->courseClassService->getAllForSelectByAcademicTerm($request->user(), $academicTermId)),
|
||||
'academicTerms' => $this->academicTermService->getAllForSelect(),
|
||||
'filters' => Inertia::always([
|
||||
'course_class_id' => $request->validated('course_class_id'),
|
||||
'academic_term_id' => $academicTermSelected
|
||||
? (string) ($academicTermId ?? 0)
|
||||
: ($academicTermId ? (string) $academicTermId : null),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -23,15 +23,27 @@ public function __construct(
|
||||
|
||||
public function index(PaginatedRequest $request): Response
|
||||
{
|
||||
$academicTermSelected = $request->has('academic_term_id');
|
||||
$academicTermId = $academicTermSelected
|
||||
? ((int) $request->validated('academic_term_id') ?: null)
|
||||
: $this->academicTermService->getActive()?->id;
|
||||
|
||||
return Inertia::render('admin/academic-classes/materials/index', [
|
||||
'materials' => Inertia::scroll(fn () => $this->service->paginated(
|
||||
$request->user(),
|
||||
...$request->validatedWithDefaults(),
|
||||
courseClassId: $request->validated('course_class_id'),
|
||||
academicTermId: $this->academicTermService->getActive()?->id,
|
||||
academicTermId: $academicTermId,
|
||||
)),
|
||||
'courseClasses' => $this->courseClassService->getAllForSelect($request->user()),
|
||||
'filters' => $request->only(['course_class_id']),
|
||||
'filterCourseClasses' => Inertia::always($this->courseClassService->getAllForSelectByAcademicTerm($request->user(), $academicTermId)),
|
||||
'academicTerms' => $this->academicTermService->getAllForSelect(),
|
||||
'filters' => Inertia::always([
|
||||
'course_class_id' => $request->validated('course_class_id'),
|
||||
'academic_term_id' => $academicTermSelected
|
||||
? (string) ($academicTermId ?? 0)
|
||||
: ($academicTermId ? (string) $academicTermId : null),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
use App\Models\User;
|
||||
use App\Services\Admin\Master\AcademicTermService;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
@ -116,8 +117,29 @@ public function duplicateFromTerm(int $sourceAcademicTermId, int $targetAcademic
|
||||
public function getAllForSelect(User $user): Collection
|
||||
{
|
||||
$isLecturer = $user->hasRole(UserRole::Dosen->value);
|
||||
$activeAcademicTermId = $isLecturer ? $this->academicTermService->getActive()?->id : null;
|
||||
$academicTermId = $isLecturer ? $this->academicTermService->getActive()?->id : null;
|
||||
|
||||
return $this->queryForSelect($user, $academicTermId)
|
||||
->get(['course_classes.id', 'course_classes.course_id', 'course_classes.lecturer_id', 'course_classes.academic_term_id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Strictly scoped to a single academic term, e.g. for a filter that
|
||||
* should only offer classes once a specific term has been chosen.
|
||||
* Returns an empty collection when no term is given.
|
||||
*/
|
||||
public function getAllForSelectByAcademicTerm(User $user, ?int $academicTermId): Collection
|
||||
{
|
||||
if (! $academicTermId) {
|
||||
return new Collection;
|
||||
}
|
||||
|
||||
return $this->queryForSelect($user, $academicTermId)
|
||||
->get(['course_classes.id', 'course_classes.course_id', 'course_classes.lecturer_id', 'course_classes.academic_term_id']);
|
||||
}
|
||||
|
||||
private function queryForSelect(User $user, ?int $academicTermId): Builder
|
||||
{
|
||||
return CourseClass::query()
|
||||
->join('courses', 'courses.id', '=', 'course_classes.course_id')
|
||||
->join('departments', 'departments.id', '=', 'courses.department_id')
|
||||
@ -125,12 +147,11 @@ public function getAllForSelect(User $user): Collection
|
||||
'course:id,code,name,semester_number,department_id',
|
||||
'course.department:id,name',
|
||||
])
|
||||
->when($isLecturer, fn ($q) => $q->where('course_classes.lecturer_id', $user->lecturer?->id)
|
||||
->where('course_classes.academic_term_id', $activeAcademicTermId))
|
||||
->when($user->hasRole(UserRole::Dosen->value), fn ($q) => $q->where('course_classes.lecturer_id', $user->lecturer?->id))
|
||||
->when($academicTermId, fn ($q) => $q->where('course_classes.academic_term_id', $academicTermId))
|
||||
->when($user->hasRole(UserRole::Mahasiswa->value), fn ($q) => $q->where('courses.department_id', $user->student?->department_id))
|
||||
->orderBy('departments.name')
|
||||
->orderBy('courses.semester_number')
|
||||
->orderBy('courses.name')
|
||||
->get(['course_classes.id', 'course_classes.course_id', 'course_classes.lecturer_id', 'course_classes.academic_term_id']);
|
||||
->orderBy('courses.name');
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,6 +45,7 @@ export type FilterField =
|
||||
placeholder?: string;
|
||||
type?: 'select';
|
||||
options: FilterOption[];
|
||||
disabled?: boolean;
|
||||
}
|
||||
| {
|
||||
key: string;
|
||||
@ -52,6 +53,7 @@ export type FilterField =
|
||||
placeholder?: string;
|
||||
type: 'combobox';
|
||||
groups: FilterOptionGroup[];
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
type FilterDialogProps = {
|
||||
@ -85,10 +87,12 @@ function ComboboxFilterField({
|
||||
isItemEqualToValue={(a: FilterOption, b: FilterOption) =>
|
||||
a.value === b.value
|
||||
}
|
||||
disabled={field.disabled}
|
||||
>
|
||||
<ComboboxInput
|
||||
placeholder={field.placeholder ?? 'Semua'}
|
||||
showClear
|
||||
disabled={field.disabled}
|
||||
className="w-full"
|
||||
/>
|
||||
<ComboboxContent>
|
||||
@ -190,6 +194,7 @@ export function FilterDialog({
|
||||
onValueChange={(value) =>
|
||||
handleChange(field.key, value)
|
||||
}
|
||||
disabled={field.disabled}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue
|
||||
|
||||
@ -45,6 +45,7 @@ import {
|
||||
submit,
|
||||
update,
|
||||
} from '@/routes/admin/academic-classes/assignments';
|
||||
import { formatAcademicTermLabel } from '@/types/academic-term';
|
||||
import type { Assignment } from '@/types/assignment';
|
||||
import { AssignmentStatuses, AssignmentStatusLabels } from '@/types/assignment';
|
||||
import { createAssignmentCard } from './card';
|
||||
@ -63,6 +64,12 @@ type CourseClassOption = {
|
||||
|
||||
type CourseClassGroup = { value: string; items: CourseClassOption[] };
|
||||
|
||||
type AcademicTermOption = {
|
||||
id: number;
|
||||
academic_year: string;
|
||||
semester: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
assignments: {
|
||||
data: Assignment[];
|
||||
@ -72,9 +79,12 @@ type Props = {
|
||||
total: number;
|
||||
};
|
||||
courseClasses: CourseClassOption[];
|
||||
filterCourseClasses: CourseClassOption[];
|
||||
academicTerms: AcademicTermOption[];
|
||||
highlight?: number;
|
||||
filters: {
|
||||
course_class_id?: string;
|
||||
academic_term_id?: string;
|
||||
};
|
||||
};
|
||||
|
||||
@ -161,6 +171,8 @@ function CourseClassField({
|
||||
export default function AssignmentIndex({
|
||||
assignments,
|
||||
courseClasses,
|
||||
filterCourseClasses,
|
||||
academicTerms,
|
||||
highlight,
|
||||
filters,
|
||||
}: Props) {
|
||||
@ -177,11 +189,24 @@ export default function AssignmentIndex({
|
||||
const canSubmit = hasPermission('submit-assignments');
|
||||
|
||||
const filterFields = [
|
||||
{
|
||||
key: 'academic_term_id',
|
||||
label: 'Periode Akademik',
|
||||
options: academicTerms.map((term) => ({
|
||||
value: String(term.id),
|
||||
label: formatAcademicTermLabel(term),
|
||||
})),
|
||||
},
|
||||
{
|
||||
key: 'course_class_id',
|
||||
label: 'Kelas',
|
||||
type: 'combobox' as const,
|
||||
groups: courseClassFilterGroups(courseClasses),
|
||||
groups: courseClassFilterGroups(filterCourseClasses),
|
||||
disabled: filterCourseClasses.length === 0,
|
||||
placeholder:
|
||||
filterCourseClasses.length === 0
|
||||
? 'Pilih periode akademik dulu'
|
||||
: undefined,
|
||||
},
|
||||
];
|
||||
|
||||
@ -199,6 +224,42 @@ export default function AssignmentIndex({
|
||||
resetKeys: ['assignments'],
|
||||
});
|
||||
|
||||
function handleApplyFilters(newFilters: Record<string, string>) {
|
||||
// Without an explicit `academic_term_id`, the backend falls back to
|
||||
// the active term, so clearing this filter needs to be sent
|
||||
// explicitly (value '0') rather than just dropping the key.
|
||||
const clearedAcademicTerm =
|
||||
Boolean(filters.academic_term_id) && !newFilters.academic_term_id;
|
||||
|
||||
const nextFilters: Record<string, string> = {
|
||||
...newFilters,
|
||||
...(clearedAcademicTerm ? { academic_term_id: '0' } : {}),
|
||||
};
|
||||
|
||||
// The class list depends on the selected term, so a previously
|
||||
// picked class is no longer relevant once the term changes.
|
||||
const academicTermChanged =
|
||||
(nextFilters.academic_term_id ?? '0') !==
|
||||
(filters.academic_term_id ?? '0');
|
||||
|
||||
if (academicTermChanged) {
|
||||
delete nextFilters.course_class_id;
|
||||
}
|
||||
|
||||
applyFilters(nextFilters);
|
||||
}
|
||||
|
||||
// '0' explicitly means "all terms" — treated as no active filter from
|
||||
// FilterDialog's point of view, so its Select shows "Semua" (not blank)
|
||||
// as selected.
|
||||
const filterDialogActiveFilters = {
|
||||
...filters,
|
||||
academic_term_id:
|
||||
filters.academic_term_id === '0'
|
||||
? undefined
|
||||
: filters.academic_term_id,
|
||||
};
|
||||
|
||||
function handleDelete() {
|
||||
if (!deleting) {
|
||||
return;
|
||||
@ -233,8 +294,8 @@ export default function AssignmentIndex({
|
||||
<>
|
||||
<FilterDialog
|
||||
fields={filterFields}
|
||||
activeFilters={filters}
|
||||
onApply={applyFilters}
|
||||
activeFilters={filterDialogActiveFilters}
|
||||
onApply={handleApplyFilters}
|
||||
/>
|
||||
<ViewToggle value={view} onChange={setView} />
|
||||
</>
|
||||
|
||||
@ -36,6 +36,7 @@ import {
|
||||
store,
|
||||
update,
|
||||
} from '@/routes/admin/academic-classes/materials';
|
||||
import { formatAcademicTermLabel } from '@/types/academic-term';
|
||||
import type { Material } from '@/types/material';
|
||||
import { createMaterialCard } from './card';
|
||||
import { createMaterialColumns } from './columns';
|
||||
@ -53,6 +54,12 @@ type CourseClassOption = {
|
||||
|
||||
type CourseClassGroup = { value: string; items: CourseClassOption[] };
|
||||
|
||||
type AcademicTermOption = {
|
||||
id: number;
|
||||
academic_year: string;
|
||||
semester: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
materials: {
|
||||
data: Material[];
|
||||
@ -62,9 +69,12 @@ type Props = {
|
||||
total: number;
|
||||
};
|
||||
courseClasses: CourseClassOption[];
|
||||
filterCourseClasses: CourseClassOption[];
|
||||
academicTerms: AcademicTermOption[];
|
||||
highlight?: number;
|
||||
filters: {
|
||||
course_class_id?: string;
|
||||
academic_term_id?: string;
|
||||
};
|
||||
};
|
||||
|
||||
@ -151,6 +161,8 @@ function courseClassFilterGroups(
|
||||
export default function MaterialIndex({
|
||||
materials,
|
||||
courseClasses,
|
||||
filterCourseClasses,
|
||||
academicTerms,
|
||||
highlight,
|
||||
filters,
|
||||
}: Props) {
|
||||
@ -164,11 +176,24 @@ export default function MaterialIndex({
|
||||
const canDelete = hasPermission('delete-materials');
|
||||
|
||||
const filterFields = [
|
||||
{
|
||||
key: 'academic_term_id',
|
||||
label: 'Periode Akademik',
|
||||
options: academicTerms.map((term) => ({
|
||||
value: String(term.id),
|
||||
label: formatAcademicTermLabel(term),
|
||||
})),
|
||||
},
|
||||
{
|
||||
key: 'course_class_id',
|
||||
label: 'Kelas',
|
||||
type: 'combobox' as const,
|
||||
groups: courseClassFilterGroups(courseClasses),
|
||||
groups: courseClassFilterGroups(filterCourseClasses),
|
||||
disabled: filterCourseClasses.length === 0,
|
||||
placeholder:
|
||||
filterCourseClasses.length === 0
|
||||
? 'Pilih periode akademik dulu'
|
||||
: undefined,
|
||||
},
|
||||
];
|
||||
|
||||
@ -186,6 +211,39 @@ export default function MaterialIndex({
|
||||
resetKeys: ['materials'],
|
||||
});
|
||||
|
||||
function handleApplyFilters(newFilters: Record<string, string>) {
|
||||
const clearedAcademicTerm =
|
||||
Boolean(filters.academic_term_id) && !newFilters.academic_term_id;
|
||||
|
||||
const nextFilters: Record<string, string> = {
|
||||
...newFilters,
|
||||
...(clearedAcademicTerm ? { academic_term_id: '0' } : {}),
|
||||
};
|
||||
|
||||
// The class list depends on the selected term, so a previously
|
||||
// picked class is no longer relevant once the term changes.
|
||||
const academicTermChanged =
|
||||
(nextFilters.academic_term_id ?? '0') !==
|
||||
(filters.academic_term_id ?? '0');
|
||||
|
||||
if (academicTermChanged) {
|
||||
delete nextFilters.course_class_id;
|
||||
}
|
||||
|
||||
applyFilters(nextFilters);
|
||||
}
|
||||
|
||||
// '0' explicitly means "all terms" — treated as no active filter from
|
||||
// FilterDialog's point of view, so its Select shows "Semua" (not blank)
|
||||
// as selected.
|
||||
const filterDialogActiveFilters = {
|
||||
...filters,
|
||||
academic_term_id:
|
||||
filters.academic_term_id === '0'
|
||||
? undefined
|
||||
: filters.academic_term_id,
|
||||
};
|
||||
|
||||
function handleDelete() {
|
||||
if (!deleting) {
|
||||
return;
|
||||
@ -214,8 +272,8 @@ export default function MaterialIndex({
|
||||
<>
|
||||
<FilterDialog
|
||||
fields={filterFields}
|
||||
activeFilters={filters}
|
||||
onApply={applyFilters}
|
||||
activeFilters={filterDialogActiveFilters}
|
||||
onApply={handleApplyFilters}
|
||||
/>
|
||||
<ViewToggle value={view} onChange={setView} />
|
||||
</>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user