From 8c681ca5f20d5e0d225f097448d8fec01ea2da27 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Wed, 11 Mar 2026 07:46:04 +0700 Subject: [PATCH] feat: Add learning material management with Material model, migration, and Filament resource. --- .../Learning/Materials/MaterialResource.php | 277 ++++++++++++++++++ .../Materials/Pages/ManageMaterials.php | 53 ++++ app/Models/Course.php | 6 + app/Models/Material.php | 26 ++ app/Providers/Filament/AdminPanelProvider.php | 2 + ...26_10_25_024508_create_materials_table.php | 33 +++ 6 files changed, 397 insertions(+) create mode 100644 app/Filament/Resources/Learning/Materials/MaterialResource.php create mode 100644 app/Filament/Resources/Learning/Materials/Pages/ManageMaterials.php create mode 100644 app/Models/Material.php create mode 100644 database/migrations/2026_10_25_024508_create_materials_table.php diff --git a/app/Filament/Resources/Learning/Materials/MaterialResource.php b/app/Filament/Resources/Learning/Materials/MaterialResource.php new file mode 100644 index 0000000..6dee6e2 --- /dev/null +++ b/app/Filament/Resources/Learning/Materials/MaterialResource.php @@ -0,0 +1,277 @@ +components([ + 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(); + }) + ->searchable() + ->live() + ->required() + ->optionsLimit(100), + + DatePicker::make('published_at') + ->label('Tanggal Publikasi') + ->placeholder('Pilih Tanggal') + ->required() + ->native(false) + ->displayFormat('l, d F Y') + ->default(now()->toDateString()), + ]) + ->columnSpanFull(), + + TextInput::make('title') + ->label('Judul') + ->placeholder('Dasar-dasar PHP') + ->autocomplete(false) + ->required() + ->maxLength(100) + ->columnSpanFull(), + + Textarea::make('description') + ->label('Deskripsi') + ->placeholder('Materi tentang pengenalan PHP...') + ->rows(3) + ->columnSpanFull(), + + AdvancedFileUpload::make('pdf') + ->label('PDF') + ->multiple() + ->reorderable() + ->pdfPreviewHeight(400) + ->pdfDisplayPage(1) + ->pdfToolbar(true) + ->pdfZoomLevel(100) + ->pdfFitType(PdfViewFit::FIT) + ->pdfNavPanes(true) + ->disk('public') + ->acceptedFileTypes(['application/pdf']) + ->maxSize(1024 * 3) + ->panelLayout('grid') + ->required() + ->columnSpanFull(), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + TextColumn::make('course.name') + ->label('Mata Kuliah') + ->searchable() + ->sortable(), + + TextColumn::make('title') + ->label('Judul') + ->searchable() + ->sortable(), + + TextColumn::make('published_at') + ->label('Tanggal Publikasi') + ->date('l, d F Y') + ->sortable(), + + TextColumn::make('description') + ->label('Deskripsi') + ->searchable() + ->sortable(), + + ...TimestampColumns::make(), + ]) + ->filters([ + SelectFilter::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() + ->optionsLimit(100), + + Filter::make('published_at') + ->schema([ + DatePicker::make('date') + ->label('Tanggal Publikasi') + ->placeholder('Pilih Tanggal') + ->native(false), + ]) + ->query(function (Builder $query, array $data): Builder { + return $query->when( + $data['date'], + fn (Builder $query, $date): Builder => $query->whereDate('published_at', $date), + ); + }), + + TrashedFilter::make() + ->native(false), + ]) + ->recordActions([ + ViewAction::make() + ->modalWidth(Width::FourExtraLarge) + ->schema(function (Material $record) { + return $record->getMedia('materials')->map(function ($item, $index) { + return PdfViewerEntry::make('pdf_'.$index) + ->hiddenLabel() + ->fileUrl($item->getUrl()) + ->columnSpanFull(); + })->toArray(); + }) + ->visible(fn (Material $record) => $record->hasMedia('materials')), + + EditAction::make() + ->modalWidth(Width::FourExtraLarge) + ->fillForm(function (Material $record): array { + $data = $record->toArray(); + + $data['pdf'] = $record->getMedia('materials') + ->map(fn ($media) => $media->getPathRelativeToRoot()) + ->toArray(); + + return $data; + }) + ->using(function (Material $record, array $data): Material { + $pdf = $data['pdf'] ?? []; + unset($data['pdf']); + + $record->update($data); + + $existingMedia = $record->getMedia('materials'); + $mediaMap = []; + + foreach ($existingMedia as $media) { + if (in_array($media->getPathRelativeToRoot(), $pdf)) { + $mediaMap[$media->getPathRelativeToRoot()] = $media; + } else { + $media->delete(); + } + } + + foreach ($pdf as $path) { + if (! isset($mediaMap[$path])) { + $newMedia = $record->addMediaFromDisk($path, config('filesystems.default')) + ->preservingOriginal() + ->withCustomProperties([ + 'feature' => 'materials', + 'date' => now()->toDateString(), + 'doc_type' => 'pdf', + ]) + ->toMediaCollection('materials'); + $mediaMap[$path] = $newMedia; + } + } + + foreach ($pdf as $index => $path) { + if (isset($mediaMap[$path])) { + $mediaMap[$path]->order_column = $index + 1; + $mediaMap[$path]->save(); + } + } + + return $record; + }), + + DeleteAction::make(), + + ForceDeleteAction::make(), + + RestoreAction::make(), + ]) + ->toolbarActions([ + BulkActionGroup::make([ + ...DefaultBulkActions::make('Materi'), + ]), + ]) + ->emptyStateIcon(Heroicon::OutlinedBookOpen) + ->emptyStateDescription('Setelah Anda membuat data pertama, maka akan muncul disini.') + ->defaultSort('created_at', 'desc') + ->deferFilters(false) + ->paginated([25, 50, 100, 'all']) + ->defaultPaginationPageOption(25); + } + + public static function getPages(): array + { + return [ + 'index' => ManageMaterials::route('/'), + ]; + } + + public static function getRecordRouteBindingEloquentQuery(): Builder + { + return parent::getRecordRouteBindingEloquentQuery() + ->withoutGlobalScopes([ + SoftDeletingScope::class, + ]); + } +} diff --git a/app/Filament/Resources/Learning/Materials/Pages/ManageMaterials.php b/app/Filament/Resources/Learning/Materials/Pages/ManageMaterials.php new file mode 100644 index 0000000..7b9aa10 --- /dev/null +++ b/app/Filament/Resources/Learning/Materials/Pages/ManageMaterials.php @@ -0,0 +1,53 @@ +label('Tambah') + ->modalHeading('Tambah Materi') + ->modalSubmitActionLabel('Simpan') + ->modalCancelActionLabel('Batal') + ->extraModalFooterActions(fn (CreateAction $action): array => [ + $action->makeModalSubmitAction('createAnother', arguments: ['another' => true]) + ->label('Simpan dan Tambah Lagi'), + ]) + ->using(function (array $data): Material { + $pdf = $data['pdf'] ?? []; + unset($data['pdf']); + + $record = Material::create($data); + + if (! empty($pdf)) { + foreach ($pdf as $filePath) { + $record->addMediaFromDisk($filePath, config('filesystems.default')) + ->preservingOriginal() + ->withCustomProperties([ + 'feature' => 'materials', + 'date' => now()->toDateString(), + 'doc_type' => 'pdf', + ]) + ->toMediaCollection('materials'); + } + } + + return $record; + }) + ->modalWidth(Width::FourExtraLarge), + ]; + } +} diff --git a/app/Models/Course.php b/app/Models/Course.php index a1dec5d..9f9e3d4 100644 --- a/app/Models/Course.php +++ b/app/Models/Course.php @@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; class Course extends Model @@ -25,4 +26,9 @@ public static function getOptionsForLecturer(Lecturer $lecturer): Collection ->where('lecturer_id', $lecturer->id) ->get(); } + + public function materials(): HasMany + { + return $this->hasMany(Material::class); + } } diff --git a/app/Models/Material.php b/app/Models/Material.php new file mode 100644 index 0000000..f9e83af --- /dev/null +++ b/app/Models/Material.php @@ -0,0 +1,26 @@ + 'datetime', + ]; + + public function course(): BelongsTo + { + return $this->belongsTo(Course::class); + } +} diff --git a/app/Providers/Filament/AdminPanelProvider.php b/app/Providers/Filament/AdminPanelProvider.php index 4bb9190..20561bc 100644 --- a/app/Providers/Filament/AdminPanelProvider.php +++ b/app/Providers/Filament/AdminPanelProvider.php @@ -86,6 +86,8 @@ public function panel(Panel $panel): Panel ->breadcrumbs(false) ->navigationGroups([ 'Master', + 'Pembelajaran', + 'Informasi', ]); } } diff --git a/database/migrations/2026_10_25_024508_create_materials_table.php b/database/migrations/2026_10_25_024508_create_materials_table.php new file mode 100644 index 0000000..f865953 --- /dev/null +++ b/database/migrations/2026_10_25_024508_create_materials_table.php @@ -0,0 +1,33 @@ +id(); + $table->foreignId('course_id')->constrained()->cascadeOnDelete(); + $table->string('title', 100); + $table->text('description')->nullable(); + $table->timestamp('published_at')->nullable(); + $table->timestamp('created_at')->useCurrent(); + $table->timestamp('updated_at')->nullable()->useCurrentOnUpdate(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('materials'); + } +};