diff --git a/app/Filament/Pages/Learning/StudyGroup/Actions/CreateStudyGroupAction.php b/app/Filament/Pages/Learning/StudyGroup/Actions/CreateStudyGroupAction.php new file mode 100644 index 0000000..dc5a9a7 --- /dev/null +++ b/app/Filament/Pages/Learning/StudyGroup/Actions/CreateStudyGroupAction.php @@ -0,0 +1,40 @@ +model(StudyGroup::class) + ->label('Tambah') + ->modalHeading('Tambah Kelompok Belajar') + ->modalSubmitActionLabel('Simpan') + ->modalCancelActionLabel('Batal') + ->extraModalFooterActions(fn (CreateAction $action): array => [ + $action->makeModalSubmitAction('createAnother', arguments: ['another' => true]) + ->label('Simpan dan Tambah Lagi'), + ]) + ->modalWidth(Width::Large) + ->schema(fn ($livewire) => $livewire->studyGroupFormSchema()) + ->after(function (StudyGroup $record, array $data) { + if (isset($data['course_id'])) { + $record->courses()->sync($data['course_id']); + } + if (isset($data['students'])) { + $record->students()->sync($data['students']); + } + }); + } +} diff --git a/app/Filament/Pages/Learning/StudyGroup/Actions/DeleteStudyGroupAction.php b/app/Filament/Pages/Learning/StudyGroup/Actions/DeleteStudyGroupAction.php new file mode 100644 index 0000000..90db7d0 --- /dev/null +++ b/app/Filament/Pages/Learning/StudyGroup/Actions/DeleteStudyGroupAction.php @@ -0,0 +1,25 @@ +record(fn (array $arguments): StudyGroup => StudyGroup::findOrFail($arguments['record'])) + ->modalHeading(fn (StudyGroup $record) => "Hapus {$record->name}") + ->modalDescription('Apakah Anda yakin ingin menghapus kelompok ini? Tindakan ini tidak dapat dibatalkan.') + ->modalSubmitActionLabel('Hapus') + ->modalCancelActionLabel('Batal'); + } +} diff --git a/app/Filament/Pages/Learning/StudyGroup/Actions/EditStudyGroupAction.php b/app/Filament/Pages/Learning/StudyGroup/Actions/EditStudyGroupAction.php new file mode 100644 index 0000000..192479f --- /dev/null +++ b/app/Filament/Pages/Learning/StudyGroup/Actions/EditStudyGroupAction.php @@ -0,0 +1,43 @@ +record(fn (array $arguments): StudyGroup => StudyGroup::findOrFail($arguments['record'])) + ->modalHeading(fn (StudyGroup $record) => "Ubah {$record->name}") + ->modalSubmitActionLabel('Simpan') + ->modalCancelActionLabel('Batal') + ->modalWidth(Width::Large) + ->schema(fn ($livewire) => $livewire->studyGroupFormSchema()) + ->fillForm(function (StudyGroup $record): array { + return [ + 'name' => $record->name, + 'leader_id' => $record->leader_id, + 'course_id' => $record->courses->pluck('id')->toArray(), + 'students' => $record->students->pluck('id')->toArray(), + ]; + }) + ->after(function (StudyGroup $record, array $data) { + if (isset($data['course_id'])) { + $record->courses()->sync($data['course_id']); + } + if (isset($data['students'])) { + $record->students()->sync($data['students']); + } + }); + } +} diff --git a/app/Filament/Pages/Learning/StudyGroup/Index.php b/app/Filament/Pages/Learning/StudyGroup/Index.php new file mode 100644 index 0000000..492ea0e --- /dev/null +++ b/app/Filament/Pages/Learning/StudyGroup/Index.php @@ -0,0 +1,214 @@ +form->fill([ + 'course_id' => $this->course_id, + ]); + } + + public function form(Schema $schema): Schema + { + return $schema + ->schema([ + Select::make('course_id') + ->label('Mata Kuliah') + ->options(function () { + return Course::all() + ->groupBy('semester') + ->mapWithKeys(function ($courses, $semester) { + return [ + "Semester $semester" => $courses->pluck('name', 'id')->toArray(), + ]; + }) + ->toArray(); + }) + ->searchable() + ->required() + ->optionsLimit(100), + + ]) + ->columns(1); + } + + protected function getHeaderActions(): array + { + return [ + CreateStudyGroupAction::make(), + ]; + } + + protected function editStudyGroupAction(): Action + { + return EditStudyGroupAction::make(); + } + + protected function deleteStudyGroupAction(): Action + { + return DeleteStudyGroupAction::make(); + } + + public function getStudyGroups(): Collection + { + $query = StudyGroup::with(['leader', 'students', 'courses']); + + if ($this->course_id) { + $query->whereHas('courses', fn ($q) => $q->where('courses.id', $this->course_id)); + } + + return $query->latest()->get(); + } + + protected function getViewData(): array + { + return [ + 'groups' => $this->getStudyGroups(), + 'studentId' => auth()->user()?->student?->id, + ]; + } + + public function isMyGroup(StudyGroup $record): bool + { + $studentId = $this->studentId; + + if (! $studentId) { + return false; + } + + return $record->leader_id === $studentId || $record->students->contains($studentId); + } + + public function studyGroupFormSchema(): array + { + return [ + Grid::make(2) + ->schema([ + Select::make('course_id') + ->label('Mata Kuliah') + ->options(function () { + return Course::all() + ->groupBy('semester') + ->mapWithKeys(function ($courses, $semester) { + return [ + "Semester $semester" => $courses->pluck('name', 'id')->toArray(), + ]; + }) + ->toArray(); + }) + ->multiple() + ->searchable() + ->required() + ->optionsLimit(100) + ->columnSpanFull(), + + TextInput::make('name') + ->label('Nama Kelompok') + ->placeholder('Kelompok A / Kelompok 1') + ->required() + ->maxLength(100) + ->autocomplete(false), + + Select::make('leader_id') + ->label('Ketua Kelompok') + ->options(function (Get $get, ?StudyGroup $record) { + $query = Student::query(); + + $courseIds = array_values(array_filter((array) ($get('courses') ?? []))); + if (! empty($courseIds)) { + $allBusyIds = StudyGroup::getBusyStudentIdsForCourses( + courseIds: $courseIds, + excludeGroupId: $record?->id, + ); + $selectedMembers = array_values(array_filter((array) ($get('students') ?? []))); + $query->whereNotIn('id', array_unique(array_merge($allBusyIds, $selectedMembers))); + } + + return $query->get()->pluck('full_name', 'id')->toArray(); + }) + ->searchable() + ->required() + ->native(false) + ->live(), + + Select::make('students') + ->label('Anggota Mahasiswa') + ->relationship( + name: 'students', + titleAttribute: 'full_name', + modifyQueryUsing: function (Builder $query, Get $get, ?StudyGroup $record) { + + $courseIds = array_values(array_filter((array) ($get('courses') ?? []))); + if (! empty($courseIds)) { + $allBusyIds = StudyGroup::getBusyStudentIdsForCourses( + courseIds: $courseIds, + excludeGroupId: $record?->id, + ); + $leaderId = $get('leader_id'); + if ($leaderId) { + $allBusyIds[] = (int) $leaderId; + } + $query->whereNotIn( + $query->getModel()->getTable().'.id', + array_unique($allBusyIds) + ); + } + + return $query->orderBy('student_number', 'asc'); + } + ) + ->multiple() + ->searchable() + ->preload() + ->native(false) + ->live() + ->columnSpanFull() + ->required(), + ]), + ]; + } +} diff --git a/app/Models/StudyGroup.php b/app/Models/StudyGroup.php new file mode 100644 index 0000000..3d241ae --- /dev/null +++ b/app/Models/StudyGroup.php @@ -0,0 +1,70 @@ +leader_id === $student->id; + } + + public function courses(): BelongsToMany + { + return $this->belongsToMany(Course::class, 'study_group_courses'); + } + + public function leader(): BelongsTo + { + return $this->belongsTo(Student::class, 'leader_id'); + } + + public function members(): HasMany + { + return $this->hasMany(StudyGroupMember::class); + } + + public function students(): BelongsToMany + { + return $this->belongsToMany(Student::class, 'study_group_members'); + } + + public static function getBusyStudentIdsForCourses(array $courseIds, ?int $excludeGroupId = null): array + { + $busyMemberIds = StudyGroupMember::whereHas( + 'studyGroup', + function ($q) use ($courseIds, $excludeGroupId) { + if ($excludeGroupId) { + $q->where('study_groups.id', '!=', $excludeGroupId); + } + $q->whereHas('courses', function ($cq) use ($courseIds) { + $cq->whereIn('courses.id', $courseIds); + }); + } + )->pluck('student_id')->toArray(); + + $busyLeaderIds = static::query() + ->when($excludeGroupId, fn ($q) => $q->where('id', '!=', $excludeGroupId)) + ->whereHas('courses', function ($cq) use ($courseIds) { + $cq->whereIn('courses.id', $courseIds); + }) + ->pluck('leader_id') + ->toArray(); + + return array_unique(array_merge($busyMemberIds, $busyLeaderIds)); + } +} diff --git a/app/Models/StudyGroupMember.php b/app/Models/StudyGroupMember.php new file mode 100644 index 0000000..ff25117 --- /dev/null +++ b/app/Models/StudyGroupMember.php @@ -0,0 +1,27 @@ +belongsTo(Student::class); + } + + public function studyGroup(): BelongsTo + { + return $this->belongsTo(StudyGroup::class); + } +} diff --git a/database/migrations/2026_02_25_024508_create_study_groups_table.php b/database/migrations/2026_02_25_024508_create_study_groups_table.php new file mode 100644 index 0000000..c6be628 --- /dev/null +++ b/database/migrations/2026_02_25_024508_create_study_groups_table.php @@ -0,0 +1,31 @@ +id(); + $table->foreignId('leader_id')->constrained('students')->cascadeOnDelete(); + $table->string('name', 100); + $table->timestamp('created_at')->useCurrent(); + $table->timestamp('updated_at')->nullable()->useCurrentOnUpdate(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('study_groups'); + } +}; diff --git a/database/migrations/2026_02_25_024509_create_study_group_members_table.php b/database/migrations/2026_02_25_024509_create_study_group_members_table.php new file mode 100644 index 0000000..f0f9419 --- /dev/null +++ b/database/migrations/2026_02_25_024509_create_study_group_members_table.php @@ -0,0 +1,31 @@ +id(); + $table->foreignId('study_group_id')->constrained()->cascadeOnDelete(); + $table->foreignId('student_id')->constrained()->cascadeOnDelete(); + $table->timestamp('created_at')->useCurrent(); + $table->timestamp('updated_at')->nullable()->useCurrentOnUpdate(); + $table->unique(['study_group_id', 'student_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('study_group_members'); + } +}; diff --git a/database/migrations/2026_03_03_072443_create_study_group_courses_table.php b/database/migrations/2026_03_03_072443_create_study_group_courses_table.php new file mode 100644 index 0000000..ab9bc28 --- /dev/null +++ b/database/migrations/2026_03_03_072443_create_study_group_courses_table.php @@ -0,0 +1,32 @@ +id(); + $table->foreignId('study_group_id')->constrained()->cascadeOnDelete(); + $table->foreignId('course_id')->constrained()->cascadeOnDelete(); + $table->unique(['study_group_id', 'course_id']); + $table->timestamp('created_at')->useCurrent(); + $table->timestamp('updated_at')->nullable()->useCurrentOnUpdate(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('study_group_courses'); + } +}; diff --git a/resources/views/filament/pages/information/list-study-groups.blade.php b/resources/views/filament/pages/information/list-study-groups.blade.php new file mode 100644 index 0000000..919b984 --- /dev/null +++ b/resources/views/filament/pages/information/list-study-groups.blade.php @@ -0,0 +1,162 @@ + + +
+
+ {{ $this->form }} +
+
+ + @if ($groups->isNotEmpty()) +
+ @foreach ($groups as $record) + @php $isMyGroup = $this->isMyGroup($record); @endphp +
+
!$isMyGroup, + 'bg-primary-50/30 dark:bg-primary-900/10 border-primary-500 shadow-md ring-1 ring-primary-500' => $isMyGroup, + ])> + + @if ($isMyGroup) +
+ + + Kelompok Saya + +
+ @endif + +
+
+ @forelse ($record->courses as $course) + !$isMyGroup, + 'bg-white dark:bg-primary-800/40 text-primary-700 dark:text-primary-200 ring-primary-500/30' => $isMyGroup, + ])> + {{ $course->name }} + + @empty + + Tanpa Mata Kuliah + + @endforelse +
+ +
+

!$isMyGroup, + 'text-primary-900 dark:text-primary-100' => $isMyGroup, + ])> + !$isMyGroup, + 'text-primary-500' => $isMyGroup, + ]) /> + {{ $record->name }} +

+
+
+
+
!$isMyGroup, + 'bg-white dark:bg-primary-800 border-primary-300 dark:border-primary-600' => $isMyGroup, + ])> + @if ($record->leader) + User avatar + @else + !$isMyGroup, + 'text-primary-700 dark:text-primary-200' => $isMyGroup, + ]) /> + @endif +
+
+ Ketua + Kelompok + !$isMyGroup, + 'text-primary-900 dark:text-primary-50' => $isMyGroup, + ])> + {{ $record->leader->full_name ?? 'Belum Ditentukan' }} + @if ($studentId && $record->leader_id === $studentId) + (Anda) + @endif + +
+
+ +
!$isMyGroup, + 'border-primary-200 dark:border-primary-800' => $isMyGroup, + ])> +
+ Anggota + Kelompok + + {{ $record->students->count() }} orang + +
+
+ @forelse ($record->students as $student) + + $student->id !== $studentId, + 'bg-primary-600 text-white dark:text-gray-900 ring-primary-700 font-bold shadow-sm' => + $student->id === $studentId, + ])> + {{ $student->full_name }} + @if ($student->id === $studentId) + (Anda) + @endif + + @empty + Belum ada anggota + @endforelse +
+
+
+
+ +
$isMyGroup, + ])> + + + + + + + +
+
+
+ @endforeach +
+ @endif + + @if ($groups->isEmpty()) + + + @endif +
+