From 19934bbe894185b1927e7c1508a31d4af8983c55 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Wed, 24 Dec 2025 08:56:11 +0700 Subject: [PATCH] feat: Implement news categories and status management with a new category resource and updated news form fields for excerpt, categories, and status. --- app/Enums/NewsStatus.php | 41 ++++++ .../Master/Categories/CategoryResource.php | 121 ++++++++++++++++++ .../Categories/Pages/ManageCategories.php | 31 +++++ .../Publication/News/Schemas/NewsForm.php | 39 ++++++ .../Publication/News/Tables/NewsTable.php | 26 +++- app/Models/Category.php | 43 +++++++ app/Models/CategoryNews.php | 21 +++ app/Models/News.php | 36 ++++++ ...5_12_11_155214_create_categories_table.php | 33 +++++ .../2025_12_12_082036_create_news_table.php | 4 + ...2_12_083015_create_category_news_table.php | 33 +++++ 11 files changed, 427 insertions(+), 1 deletion(-) create mode 100644 app/Enums/NewsStatus.php create mode 100644 app/Filament/Resources/Master/Categories/CategoryResource.php create mode 100644 app/Filament/Resources/Master/Categories/Pages/ManageCategories.php create mode 100644 app/Models/Category.php create mode 100644 app/Models/CategoryNews.php create mode 100644 database/migrations/2025_12_11_155214_create_categories_table.php create mode 100644 database/migrations/2025_12_12_083015_create_category_news_table.php diff --git a/app/Enums/NewsStatus.php b/app/Enums/NewsStatus.php new file mode 100644 index 0000000..40af2d4 --- /dev/null +++ b/app/Enums/NewsStatus.php @@ -0,0 +1,41 @@ + 'Draft', + self::PUBLISHED => 'Publish', + self::ARCHIVED => 'Arsip', + }; + } + + public function getColor(): string|array|null + { + return match ($this) { + self::DRAFT => 'warning', + self::PUBLISHED => 'success', + self::ARCHIVED => 'gray', + }; + } + + public static function options(): array + { + return collect(self::cases()) + ->mapWithKeys(fn ($case) => [$case->value => $case->getLabel()]) + ->toArray(); + } +} diff --git a/app/Filament/Resources/Master/Categories/CategoryResource.php b/app/Filament/Resources/Master/Categories/CategoryResource.php new file mode 100644 index 0000000..37f43bb --- /dev/null +++ b/app/Filament/Resources/Master/Categories/CategoryResource.php @@ -0,0 +1,121 @@ +components([ + TextInput::make('name') + ->label('Nama') + ->placeholder('Ekonomi') + ->autocomplete(false) + ->autofocus() + ->required() + ->maxLength(50), + ]) + ->columns(1); + } + + public static function table(Table $table): Table + { + return $table + ->recordTitleAttribute('name') + ->columns([ + TextColumn::make('name') + ->label('Nama') + ->searchable() + ->sortable(), + + ToggleColumn::make('is_active') + ->label('Aktif?') + ->sortable() + ->getStateUsing(fn (Category $record) => $record->is_active === IsActive::ACTIVE) + ->updateStateUsing(function (Category $record, bool $state) { + $record->is_active = $state ? IsActive::ACTIVE : IsActive::INACTIVE; + $record->save(); + }), + + ...TimestampColumns::make(), + ]) + ->filters([ + TrashedFilter::make() + ->native(false), + ]) + ->recordActions([ + EditAction::make() + ->modalWidth(Width::Large), + + DeleteAction::make(), + + ForceDeleteAction::make(), + + RestoreAction::make(), + ]) + ->toolbarActions([ + BulkActionGroup::make([ + ...DefaultBulkActions::make('kategori berita'), + ]), + ]) + ->emptyStateIcon(Heroicon::ListBullet) + ->emptyStateDescription('Setelah Anda membubat data pertama, maka akan muncul disini.') + ->defaultSort('created_at', 'desc') + ->deferFilters(false) + ->reorderable('sort_order'); + } + + public static function getPages(): array + { + return [ + 'index' => ManageCategories::route('/'), + ]; + } + + public static function getRecordRouteBindingEloquentQuery(): Builder + { + return parent::getRecordRouteBindingEloquentQuery() + ->withoutGlobalScopes([ + SoftDeletingScope::class, + ]); + } +} diff --git a/app/Filament/Resources/Master/Categories/Pages/ManageCategories.php b/app/Filament/Resources/Master/Categories/Pages/ManageCategories.php new file mode 100644 index 0000000..f77a67d --- /dev/null +++ b/app/Filament/Resources/Master/Categories/Pages/ManageCategories.php @@ -0,0 +1,31 @@ +label('Tambah') + ->modalHeading('Tambah Kategori Berita') + ->modalSubmitActionLabel('Simpan') + ->modalCancelActionLabel('Batal') + ->extraModalFooterActions(fn (CreateAction $action): array => [ + $action->makeModalSubmitAction('createAnother', arguments: ['another' => true]) + ->label('Simpan dan Tambah Lagi'), + ]) + ->modalWidth(Width::Large), + ]; + } +} diff --git a/app/Filament/Resources/Publication/News/Schemas/NewsForm.php b/app/Filament/Resources/Publication/News/Schemas/NewsForm.php index 523d30f..da8b8a5 100644 --- a/app/Filament/Resources/Publication/News/Schemas/NewsForm.php +++ b/app/Filament/Resources/Publication/News/Schemas/NewsForm.php @@ -2,11 +2,17 @@ namespace App\Filament\Resources\Publication\News\Schemas; +use App\Enums\NewsStatus; +use App\Models\News; +use Filament\Forms\Components\Hidden; +use Filament\Forms\Components\Radio; use Filament\Forms\Components\RichEditor; +use Filament\Forms\Components\Select; use Filament\Forms\Components\SpatieMediaLibraryFileUpload; use Filament\Forms\Components\TagsInput; use Filament\Forms\Components\TextInput; use Filament\Schemas\Components\Section; +use Filament\Schemas\Components\Utilities\Set; use Filament\Schemas\Schema; class NewsForm @@ -30,6 +36,12 @@ public static function configure(Schema $schema): Schema ->required() ->columnSpanFull(), + RichEditor::make('excerpt') + ->label('Ringkasan') + ->placeholder('...') + ->required() + ->columnSpanFull(), + TextInput::make('link') ->label('Tautan') ->placeholder('https://www.pwknews.com') @@ -40,6 +52,14 @@ public static function configure(Schema $schema): Schema ])->columnSpan(2), Section::make([ + Select::make('categories') + ->label('Kategori') + ->relationship('categories', 'name', fn ($query) => $query->active()) + ->native(false) + ->multiple() + ->preload() + ->required(), + TagsInput::make('tags') ->reactive() ->afterStateHydrated(function ($component, $state, $record) { @@ -51,6 +71,25 @@ public static function configure(Schema $schema): Schema ); }), + Radio::make('status') + ->label('Status') + ->required() + ->options(NewsStatus::options()) + ->default(NewsStatus::PUBLISHED->value) + ->inline() + ->live() + ->afterStateUpdated(function (Set $set, ?string $state, ?News $news) { + if ($state == NewsStatus::PUBLISHED->value && ! $news?->published_at) { + $set('published_at', now()); + } elseif ($state != NewsStatus::PUBLISHED->value && ! $news) { + $set('published_at', null); + } + }) + ->rules(['in:'.implode(',', NewsStatus::values())]), + + Hidden::make('published_at') + ->default(now()), + SpatieMediaLibraryFileUpload::make('thumbnail') ->label('Thumbnail') ->disk(config('filesystems.default')) diff --git a/app/Filament/Resources/Publication/News/Tables/NewsTable.php b/app/Filament/Resources/Publication/News/Tables/NewsTable.php index 09817db..d21b7b9 100644 --- a/app/Filament/Resources/Publication/News/Tables/NewsTable.php +++ b/app/Filament/Resources/Publication/News/Tables/NewsTable.php @@ -39,6 +39,14 @@ public static function configure(Table $table): Table ->sortable() ->alignCenter(), + TextColumn::make('categories.name') + ->label('Kategori') + ->listWithLineBreaks() + ->limitList(3) + ->expandableLimitedList() + ->badge() + ->placeholder('Tidak ada tag'), + TextColumn::make('tags.name') ->label('Tag') ->listWithLineBreaks() @@ -53,7 +61,23 @@ public static function configure(Table $table): Table ->sortable() ->copyable() ->copyMessage('Tautan berhasil disalin') - ->placeholder('Tidak ada'), + ->placeholder('Tidak ada') + ->toggleable(isToggledHiddenByDefault: true), + + TextColumn::make('status') + ->label('Status') + ->searchable() + ->sortable() + ->badge() + ->formatStateUsing(fn ($state) => $state->getLabel()) + ->color(fn ($state) => $state->getColor()), + + TextColumn::make('published_at') + ->label('Tgl Publish') + ->searchable() + ->sortable() + ->date('l, d F Y H:i') + ->toggleable(isToggledHiddenByDefault: true), ...TimestampColumns::make(), ]) diff --git a/app/Models/Category.php b/app/Models/Category.php new file mode 100644 index 0000000..1f9ed72 --- /dev/null +++ b/app/Models/Category.php @@ -0,0 +1,43 @@ + IsActive::class, + 'sort_order' => 'integer', + ]; + } + + #[Scope] + protected function active(Builder $query): void + { + $query->where('is_active', IsActive::ACTIVE); + } + + #[Scope] + protected function inactive(Builder $query): void + { + $query->where('is_active', IsActive::INACTIVE); + } + + public function news(): BelongsToMany + { + return $this->belongsToMany(News::class); + } +} diff --git a/app/Models/CategoryNews.php b/app/Models/CategoryNews.php new file mode 100644 index 0000000..6fea5e1 --- /dev/null +++ b/app/Models/CategoryNews.php @@ -0,0 +1,21 @@ +belongsToMany(Category::class); + } + + public function news(): BelongsToMany + { + return $this->belongsToMany(News::class); + } +} diff --git a/app/Models/News.php b/app/Models/News.php index cd392ec..3ed3987 100644 --- a/app/Models/News.php +++ b/app/Models/News.php @@ -2,9 +2,13 @@ namespace App\Models; +use App\Enums\NewsStatus; +use Illuminate\Database\Eloquent\Attributes\Scope; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\SoftDeletes; use Spatie\MediaLibrary\HasMedia; use Spatie\MediaLibrary\InteractsWithMedia; @@ -18,6 +22,33 @@ class News extends Model implements HasMedia protected $guarded = ['id']; + protected function casts(): array + { + return [ + 'views' => 'int', + 'status' => NewsStatus::class, + 'published_at' => 'datetime', + ]; + } + + #[Scope] + protected function published(Builder $query): void + { + $query->where('status', NewsStatus::PUBLISHED); + } + + #[Scope] + protected function draft(Builder $query): void + { + $query->where('status', NewsStatus::DRAFT); + } + + #[Scope] + protected function archived(Builder $query): void + { + $query->where('status', NewsStatus::ARCHIVED); + } + public function getSlugOptions(): SlugOptions { return SlugOptions::create() @@ -29,4 +60,9 @@ public function author(): BelongsTo { return $this->belongsTo(User::class, 'author_id'); } + + public function categories(): BelongsToMany + { + return $this->belongsToMany(Category::class); + } } diff --git a/database/migrations/2025_12_11_155214_create_categories_table.php b/database/migrations/2025_12_11_155214_create_categories_table.php new file mode 100644 index 0000000..570a93c --- /dev/null +++ b/database/migrations/2025_12_11_155214_create_categories_table.php @@ -0,0 +1,33 @@ +id(); + $table->string('name', 50); + $table->enum('is_active', [IsActive::values()])->default(IsActive::ACTIVE->value)->comment(IsActive::comment()); + $table->integer('sort_order')->default(0); + $table->timestamp('created_at')->useCurrent(); + $table->timestamp('updated_at')->nullable()->useCurrentOnUpdate(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('categories'); + } +}; diff --git a/database/migrations/2025_12_12_082036_create_news_table.php b/database/migrations/2025_12_12_082036_create_news_table.php index 45b52a1..dd97848 100644 --- a/database/migrations/2025_12_12_082036_create_news_table.php +++ b/database/migrations/2025_12_12_082036_create_news_table.php @@ -1,5 +1,6 @@ string('title', 200); $table->string('slug', 200); $table->text('content'); + $table->text('excerpt'); $table->string('link', 50)->nullable(); $table->unsignedInteger('view')->default(0); + $table->enum('status', [NewsStatus::values()])->comment(NewsStatus::comment()); + $table->timestamp('published_at')->nullable(); $table->timestamp('created_at')->useCurrent(); $table->timestamp('updated_at')->nullable()->useCurrentOnUpdate(); $table->softDeletes(); diff --git a/database/migrations/2025_12_12_083015_create_category_news_table.php b/database/migrations/2025_12_12_083015_create_category_news_table.php new file mode 100644 index 0000000..31ec40e --- /dev/null +++ b/database/migrations/2025_12_12_083015_create_category_news_table.php @@ -0,0 +1,33 @@ +id(); + $table->foreignIdFor(Category::class); + $table->foreignIdFor(News::class); + $table->timestamp('created_at')->useCurrent(); + $table->timestamp('updated_at')->nullable()->useCurrentOnUpdate(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('category_news'); + } +};