diff --git a/app/Http/Controllers/Admin/AcademicClasses/Assignment/AssignmentController.php b/app/Http/Controllers/Admin/AcademicClasses/Assignment/AssignmentController.php
index 25538b3..bac5505 100644
--- a/app/Http/Controllers/Admin/AcademicClasses/Assignment/AssignmentController.php
+++ b/app/Http/Controllers/Admin/AcademicClasses/Assignment/AssignmentController.php
@@ -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),
+ ]),
]);
}
diff --git a/app/Http/Controllers/Admin/AcademicClasses/MaterialController.php b/app/Http/Controllers/Admin/AcademicClasses/MaterialController.php
index 9cc4b4d..470d701 100644
--- a/app/Http/Controllers/Admin/AcademicClasses/MaterialController.php
+++ b/app/Http/Controllers/Admin/AcademicClasses/MaterialController.php
@@ -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),
+ ]),
]);
}
diff --git a/app/Services/Admin/Manage/CourseClass/CourseClassService.php b/app/Services/Admin/Manage/CourseClass/CourseClassService.php
index 8145560..0cceef0 100644
--- a/app/Services/Admin/Manage/CourseClass/CourseClassService.php
+++ b/app/Services/Admin/Manage/CourseClass/CourseClassService.php
@@ -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');
}
}
diff --git a/resources/js/components/filter-dialog.tsx b/resources/js/components/filter-dialog.tsx
index 626a1ce..47d504b 100644
--- a/resources/js/components/filter-dialog.tsx
+++ b/resources/js/components/filter-dialog.tsx
@@ -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}
>
@@ -190,6 +194,7 @@ export function FilterDialog({
onValueChange={(value) =>
handleChange(field.key, value)
}
+ disabled={field.disabled}
>
({
+ 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) {
+ // 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 = {
+ ...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({
<>
>
diff --git a/resources/js/pages/admin/academic-classes/materials/index.tsx b/resources/js/pages/admin/academic-classes/materials/index.tsx
index d8aa235..ff0bb71 100644
--- a/resources/js/pages/admin/academic-classes/materials/index.tsx
+++ b/resources/js/pages/admin/academic-classes/materials/index.tsx
@@ -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) {
+ const clearedAcademicTerm =
+ Boolean(filters.academic_term_id) && !newFilters.academic_term_id;
+
+ const nextFilters: Record = {
+ ...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({
<>
>