From a7e10600d482b64689527395ee4832677f9d24f1 Mon Sep 17 00:00:00 2001 From: tuxarmy Date: Fri, 3 Apr 2026 06:48:06 +0700 Subject: [PATCH] feat: anggota dapat menulis artikel dengan workflow approval sebelum diterbitkan --- .../Resources/MyPosts/MyPostResource.php | 46 ++++++++++++ .../Resources/MyPosts/Pages/CreateMyPost.php | 11 +++ .../Resources/MyPosts/Pages/EditMyPost.php | 19 +++++ .../Resources/MyPosts/Pages/ListMyPosts.php | 19 +++++ .../Resources/MyPosts/Schemas/MyPostForm.php | 30 ++++++++ .../Resources/MyPosts/Tables/MyPostsTable.php | 59 +++++++++++++++ .../Resources/Posts/Tables/PostsTable.php | 48 ++++++++++-- app/Models/Post.php | 12 ++- app/Policies/ContactMessagePolicy.php | 75 +++++++++++++++++++ ...04_02_233606_add_status_to_posts_table.php | 25 +++++++ database/seeders/PermissionSeeder.php | 3 +- database/seeders/PostSeeder.php | 2 +- 12 files changed, 339 insertions(+), 10 deletions(-) create mode 100644 app/Filament/Resources/MyPosts/MyPostResource.php create mode 100644 app/Filament/Resources/MyPosts/Pages/CreateMyPost.php create mode 100644 app/Filament/Resources/MyPosts/Pages/EditMyPost.php create mode 100644 app/Filament/Resources/MyPosts/Pages/ListMyPosts.php create mode 100644 app/Filament/Resources/MyPosts/Schemas/MyPostForm.php create mode 100644 app/Filament/Resources/MyPosts/Tables/MyPostsTable.php create mode 100644 app/Policies/ContactMessagePolicy.php create mode 100644 database/migrations/2026_04_02_233606_add_status_to_posts_table.php diff --git a/app/Filament/Resources/MyPosts/MyPostResource.php b/app/Filament/Resources/MyPosts/MyPostResource.php new file mode 100644 index 0000000..f2b43f3 --- /dev/null +++ b/app/Filament/Resources/MyPosts/MyPostResource.php @@ -0,0 +1,46 @@ +where('author_id', auth()->id()); + } + + public static function form(Schema $form): Schema + { + return \App\Filament\Resources\MyPosts\Schemas\MyPostForm::configure($form); + } + + public static function table(Table $table): Table + { + return \App\Filament\Resources\MyPosts\Tables\MyPostsTable::configure($table); + } + + public static function getPages(): array + { + return [ + 'index' => ListMyPosts::route('/'), + 'create' => CreateMyPost::route('/create'), + 'edit' => EditMyPost::route('/{record}/edit'), + ]; + } +} diff --git a/app/Filament/Resources/MyPosts/Pages/CreateMyPost.php b/app/Filament/Resources/MyPosts/Pages/CreateMyPost.php new file mode 100644 index 0000000..500783c --- /dev/null +++ b/app/Filament/Resources/MyPosts/Pages/CreateMyPost.php @@ -0,0 +1,11 @@ +components([ + TextInput::make('title')->label('Judul')->required() + ->live(onBlur: true) + ->afterStateUpdated(fn ($state, $set) => $set('slug', Str::slug($state))), + TextInput::make('slug')->required()->unique(ignoreRecord: true), + Select::make('category')->label('Kategori') + ->options([ + 'umum' => 'Umum', + 'pengumuman' => 'Pengumuman', + 'berita' => 'Berita', + ]) + ->default('umum')->required(), + RichEditor::make('content')->label('Konten')->required()->columnSpanFull(), + ]); + } +} diff --git a/app/Filament/Resources/MyPosts/Tables/MyPostsTable.php b/app/Filament/Resources/MyPosts/Tables/MyPostsTable.php new file mode 100644 index 0000000..6c3854b --- /dev/null +++ b/app/Filament/Resources/MyPosts/Tables/MyPostsTable.php @@ -0,0 +1,59 @@ +columns([ + TextColumn::make('title')->label('Judul')->searchable()->sortable(), + TextColumn::make('category')->label('Kategori')->badge(), + TextColumn::make('status')->badge() + ->color(fn ($state) => match ($state) { + 'published' => 'success', + 'pending' => 'warning', + 'rejected' => 'danger', + default => 'gray', + }), + TextColumn::make('rejection_reason')->label('Alasan Penolakan') + ->visible(fn ($record) => $record?->status === 'rejected') + ->limit(40)->default('-'), + TextColumn::make('created_at')->label('Dibuat')->date('d M Y')->sortable(), + ]) + ->recordActions([ + // Hanya bisa edit jika masih draft atau rejected + EditAction::make() + ->visible(fn ($record) => in_array($record->status, ['draft', 'rejected'])), + // Ajukan untuk review + Action::make('submit') + ->label('Ajukan') + ->icon('heroicon-o-paper-airplane') + ->color('info') + ->requiresConfirmation() + ->visible(fn ($record) => in_array($record->status, ['draft', 'rejected'])) + ->action(fn ($record) => $record->update(['status' => 'pending', 'rejection_reason' => null])), + ]) + ->toolbarActions([ + BulkActionGroup::make([ + DeleteBulkAction::make() + ->before(function ($records) { + // Tidak bisa hapus yang sudah published + $records->each(function ($record) { + if ($record->status === 'published') { + throw new \Exception("Artikel yang sudah diterbitkan tidak dapat dihapus."); + } + }); + }), + ]), + ]); + } +} diff --git a/app/Filament/Resources/Posts/Tables/PostsTable.php b/app/Filament/Resources/Posts/Tables/PostsTable.php index 21cf0d5..a70b095 100644 --- a/app/Filament/Resources/Posts/Tables/PostsTable.php +++ b/app/Filament/Resources/Posts/Tables/PostsTable.php @@ -2,9 +2,11 @@ namespace App\Filament\Resources\Posts\Tables; +use Filament\Actions\Action; use Filament\Actions\BulkActionGroup; use Filament\Actions\DeleteBulkAction; use Filament\Actions\EditAction; +use Filament\Forms\Components\Textarea; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Table; @@ -22,18 +24,52 @@ class PostsTable 'berita' => 'info', default => 'gray', }), + TextColumn::make('status')->badge() + ->color(fn ($state) => match ($state) { + 'published' => 'success', + 'pending' => 'warning', + 'rejected' => 'danger', + default => 'gray', + }), TextColumn::make('author.name')->label('Penulis'), TextColumn::make('published_at')->label('Dipublikasi') - ->dateTime('d M Y')->default('Draft')->sortable(), + ->dateTime('d M Y')->default('-')->sortable(), ]) ->filters([ - SelectFilter::make('category')->options([ - 'umum' => 'Umum', - 'pengumuman' => 'Pengumuman', - 'berita' => 'Berita', + SelectFilter::make('status')->options([ + 'draft' => 'Draft', + 'pending' => 'Menunggu', + 'published' => 'Diterbitkan', + 'rejected' => 'Ditolak', ]), ]) - ->recordActions([EditAction::make()]) + ->recordActions([ + Action::make('publish') + ->label('Terbitkan') + ->icon('heroicon-o-check-circle') + ->color('success') + ->requiresConfirmation() + ->visible(fn ($record) => $record->status === 'pending') + ->action(fn ($record) => $record->update([ + 'status' => 'published', + 'published_at' => now(), + 'reviewed_by' => auth()->id(), + ])), + Action::make('reject') + ->label('Tolak') + ->icon('heroicon-o-x-circle') + ->color('danger') + ->visible(fn ($record) => $record->status === 'pending') + ->form([ + Textarea::make('rejection_reason')->label('Alasan Penolakan')->required(), + ]) + ->action(fn ($record, array $data) => $record->update([ + 'status' => 'rejected', + 'reviewed_by' => auth()->id(), + 'rejection_reason' => $data['rejection_reason'], + ])), + EditAction::make(), + ]) ->toolbarActions([BulkActionGroup::make([DeleteBulkAction::make()])]); } } diff --git a/app/Models/Post.php b/app/Models/Post.php index b59406c..dc874e6 100644 --- a/app/Models/Post.php +++ b/app/Models/Post.php @@ -8,7 +8,7 @@ use Illuminate\Support\Str; class Post extends Model { - protected $fillable = ['title', 'slug', 'category', 'content', 'author_id', 'published_at']; + protected $fillable = ['title', 'slug', 'category', 'content', 'author_id', 'published_at', 'status', 'reviewed_by', 'rejection_reason']; protected $casts = ['published_at' => 'datetime']; @@ -16,6 +16,7 @@ class Post extends Model { static::creating(function (Post $post) { $post->slug ??= Str::slug($post->title); + $post->author_id ??= auth()->id(); }); } @@ -24,8 +25,15 @@ class Post extends Model return $this->belongsTo(User::class, 'author_id'); } + public function reviewer(): BelongsTo + { + return $this->belongsTo(User::class, 'reviewed_by'); + } + public function scopePublished($query) { - return $query->whereNotNull('published_at')->where('published_at', '<=', now()); + return $query->where('status', 'published') + ->whereNotNull('published_at') + ->where('published_at', '<=', now()); } } diff --git a/app/Policies/ContactMessagePolicy.php b/app/Policies/ContactMessagePolicy.php new file mode 100644 index 0000000..02014c0 --- /dev/null +++ b/app/Policies/ContactMessagePolicy.php @@ -0,0 +1,75 @@ +can('ViewAny:ContactMessage'); + } + + public function view(AuthUser $authUser, ContactMessage $contactMessage): bool + { + return $authUser->can('View:ContactMessage'); + } + + public function create(AuthUser $authUser): bool + { + return $authUser->can('Create:ContactMessage'); + } + + public function update(AuthUser $authUser, ContactMessage $contactMessage): bool + { + return $authUser->can('Update:ContactMessage'); + } + + public function delete(AuthUser $authUser, ContactMessage $contactMessage): bool + { + return $authUser->can('Delete:ContactMessage'); + } + + public function deleteAny(AuthUser $authUser): bool + { + return $authUser->can('DeleteAny:ContactMessage'); + } + + public function restore(AuthUser $authUser, ContactMessage $contactMessage): bool + { + return $authUser->can('Restore:ContactMessage'); + } + + public function forceDelete(AuthUser $authUser, ContactMessage $contactMessage): bool + { + return $authUser->can('ForceDelete:ContactMessage'); + } + + public function forceDeleteAny(AuthUser $authUser): bool + { + return $authUser->can('ForceDeleteAny:ContactMessage'); + } + + public function restoreAny(AuthUser $authUser): bool + { + return $authUser->can('RestoreAny:ContactMessage'); + } + + public function replicate(AuthUser $authUser, ContactMessage $contactMessage): bool + { + return $authUser->can('Replicate:ContactMessage'); + } + + public function reorder(AuthUser $authUser): bool + { + return $authUser->can('Reorder:ContactMessage'); + } + +} \ No newline at end of file diff --git a/database/migrations/2026_04_02_233606_add_status_to_posts_table.php b/database/migrations/2026_04_02_233606_add_status_to_posts_table.php new file mode 100644 index 0000000..1280c11 --- /dev/null +++ b/database/migrations/2026_04_02_233606_add_status_to_posts_table.php @@ -0,0 +1,25 @@ +enum('status', ['draft', 'pending', 'published', 'rejected']) + ->default('draft')->after('author_id'); + $table->foreignId('reviewed_by')->nullable()->constrained('users')->after('status'); + $table->text('rejection_reason')->nullable()->after('reviewed_by'); + }); + } + + public function down(): void + { + Schema::table('posts', function (Blueprint $table) { + $table->dropColumn(['status', 'reviewed_by', 'rejection_reason']); + }); + } +}; diff --git a/database/seeders/PermissionSeeder.php b/database/seeders/PermissionSeeder.php index 14e7bf5..628e1d9 100644 --- a/database/seeders/PermissionSeeder.php +++ b/database/seeders/PermissionSeeder.php @@ -36,10 +36,11 @@ class PermissionSeeder extends Seeder ->orWhere('name', 'like', 'View:Division') ->get()); - // Anggota: hanya lihat kegiatan & voting + // Anggota: lihat kegiatan & voting + kelola artikel sendiri $anggota->syncPermissions(Permission::whereIn('name', [ 'ViewAny:Activity', 'View:Activity', 'ViewAny:Vote', 'View:Vote', + 'ViewAny:MyPost', 'View:MyPost', 'Create:MyPost', 'Update:MyPost', 'Delete:MyPost', ])->get()); // Auditor: read-only semua + akses audit diff --git a/database/seeders/PostSeeder.php b/database/seeders/PostSeeder.php index 89c0cdf..bbb5010 100644 --- a/database/seeders/PostSeeder.php +++ b/database/seeders/PostSeeder.php @@ -36,7 +36,7 @@ class PostSeeder extends Seeder foreach ($posts as $data) { Post::firstOrCreate( ['slug' => \Illuminate\Support\Str::slug($data['title'])], - array_merge($data, ['author_id' => $author->id]) + array_merge($data, ['author_id' => $author->id, 'status' => 'published']) ); } }