feat: Implement news categories and status management with a new category resource and updated news form fields for excerpt, categories, and status.

This commit is contained in:
Yoga Pangestu 2025-12-24 08:56:11 +07:00
parent 4faaebdc59
commit 19934bbe89
11 changed files with 427 additions and 1 deletions

41
app/Enums/NewsStatus.php Normal file
View File

@ -0,0 +1,41 @@
<?php
namespace App\Enums;
use App\Traits\Enum\WithComment;
use App\Traits\Enum\WithValue;
use Filament\Support\Contracts\HasLabel;
enum NewsStatus: int implements HasLabel
{
use WithComment, WithValue;
case DRAFT = 1;
case PUBLISHED = 2;
case ARCHIVED = 3;
public function getLabel(): ?string
{
return match ($this) {
self::DRAFT => '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();
}
}

View File

@ -0,0 +1,121 @@
<?php
namespace App\Filament\Resources\Master\Categories;
use App\Enums\IsActive;
use App\Filament\Actions\DefaultBulkActions;
use App\Filament\Columns\TimestampColumns;
use App\Filament\Resources\Master\Categories\Pages\ManageCategories;
use App\Models\Category;
use BackedEnum;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction;
use Filament\Actions\ForceDeleteAction;
use Filament\Actions\RestoreAction;
use Filament\Forms\Components\TextInput;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Enums\Width;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\ToggleColumn;
use Filament\Tables\Filters\TrashedFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use UnitEnum;
class CategoryResource extends Resource
{
protected static ?string $model = Category::class;
protected static string|UnitEnum|null $navigationGroup = 'Master';
protected static string|BackedEnum|null $navigationIcon = Heroicon::ListBullet;
protected static ?string $navigationLabel = 'Kategori Berita';
protected static ?int $navigationSort = 7;
protected static ?string $recordTitleAttribute = 'name';
protected static ?string $slug = 'master/categories';
public static function form(Schema $schema): Schema
{
return $schema
->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,
]);
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Filament\Resources\Master\Categories\Pages;
use App\Filament\Resources\Master\Categories\CategoryResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ManageRecords;
use Filament\Support\Enums\Width;
class ManageCategories extends ManageRecords
{
protected static ?string $title = 'Kategori Berita';
protected static string $resource = CategoryResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make()
->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),
];
}
}

View File

@ -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'))

View File

@ -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(),
])

43
app/Models/Category.php Normal file
View File

@ -0,0 +1,43 @@
<?php
namespace App\Models;
use App\Enums\IsActive;
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\BelongsToMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class Category extends Model
{
use HasFactory, SoftDeletes;
protected $guarded = ['id'];
protected function casts(): array
{
return [
'is_active' => 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);
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class CategoryNews extends Model
{
protected $guarded = ['id'];
public function category(): BelongsToMany
{
return $this->belongsToMany(Category::class);
}
public function news(): BelongsToMany
{
return $this->belongsToMany(News::class);
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,33 @@
<?php
use App\Enums\IsActive;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('categories', function (Blueprint $table) {
$table->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');
}
};

View File

@ -1,5 +1,6 @@
<?php
use App\Enums\NewsStatus;
use App\Models\User;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
@ -18,8 +19,11 @@ public function up(): void
$table->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();

View File

@ -0,0 +1,33 @@
<?php
use App\Models\Category;
use App\Models\News;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('category_news', function (Blueprint $table) {
$table->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');
}
};