From 8b8ce3077b4c6090460b9d29bb6e8e4fbd16894e Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Fri, 3 Oct 2025 13:48:40 +0700 Subject: [PATCH] feat(article): implementasi crud -membuat permission di seeder -membuat enum untuk status --- app/Enums/ArticleStatus.php | 33 +++++++ .../Datatable/Studio/Manage/ArticlesTable.php | 61 ++++++++++++ .../Forms/Studio/Manage/ArticleForm.php | 98 +++++++++++++++++++ app/Livewire/Studio/Manage/Article/Create.php | 38 +++++++ app/Livewire/Studio/Manage/Article/Edit.php | 44 +++++++++ app/Livewire/Studio/Manage/Article/Index.php | 34 +++++++ app/Models/Article.php | 42 ++++++++ app/Models/User.php | 6 ++ app/Providers/ViewServiceProvider.php | 12 +++ ...025_10_03_025736_create_articles_table.php | 38 +++++++ database/seeders/RolePermissionSeeder.php | 10 ++ .../studio/manage/article/form.blade.php | 88 +++++++++++++++++ .../studio/manage/article/index.blade.php | 21 ++++ routes/pages/studio.php | 12 +++ 14 files changed, 537 insertions(+) create mode 100644 app/Enums/ArticleStatus.php create mode 100644 app/Livewire/Datatable/Studio/Manage/ArticlesTable.php create mode 100644 app/Livewire/Forms/Studio/Manage/ArticleForm.php create mode 100644 app/Livewire/Studio/Manage/Article/Create.php create mode 100644 app/Livewire/Studio/Manage/Article/Edit.php create mode 100644 app/Livewire/Studio/Manage/Article/Index.php create mode 100644 app/Models/Article.php create mode 100644 database/migrations/2025_10_03_025736_create_articles_table.php create mode 100644 resources/views/livewire/studio/manage/article/form.blade.php create mode 100644 resources/views/livewire/studio/manage/article/index.blade.php diff --git a/app/Enums/ArticleStatus.php b/app/Enums/ArticleStatus.php new file mode 100644 index 0000000..94e3c7e --- /dev/null +++ b/app/Enums/ArticleStatus.php @@ -0,0 +1,33 @@ + 'Publish', + self::DRAFT => 'Draft', + self::ARCHIVED => 'Arsip', + }; + } + + public function color() + { + return match ($this) { + self::PUBLISHED => 'emerald', + self::DRAFT => 'yellow', + self::ARCHIVED => 'rose', + }; + } +} diff --git a/app/Livewire/Datatable/Studio/Manage/ArticlesTable.php b/app/Livewire/Datatable/Studio/Manage/ArticlesTable.php new file mode 100644 index 0000000..a024db4 --- /dev/null +++ b/app/Livewire/Datatable/Studio/Manage/ArticlesTable.php @@ -0,0 +1,61 @@ +searchable(), + + Column::make('Judul', 'title')->searchable(), + + Column::make('Status', 'status') + ->format( + fn($value) => Blade::render('' . $value->label() . '') + ) + ->html(), + + Column::make('Aksi') + ->label(function ($row) { + $actions = ''; + + if (auth()->user()->can('update article')) { + $actions .= view('components.datatables.edit', [ + 'id' => $row->hash, + 'editRoute' => route('studio.manage.article.edit', $row->hash), + ])->render(); + } + + if (auth()->user()->can('delete article')) { + $actions .= view('components.datatables.delete', [ + 'id' => $row->hash, + 'deleteRoute' => route('studio.manage.article.delete', $row->hash), + ])->render(); + } + + return $actions; + }) + ->html(), + ]; + } + + public function builder(): Builder + { + return Article::select('articles.id', 'title', 'articles.status')->with(['author', 'author.employee']); + } +} diff --git a/app/Livewire/Forms/Studio/Manage/ArticleForm.php b/app/Livewire/Forms/Studio/Manage/ArticleForm.php new file mode 100644 index 0000000..62bff6e --- /dev/null +++ b/app/Livewire/Forms/Studio/Manage/ArticleForm.php @@ -0,0 +1,98 @@ + ['required', 'string', 'max:200'], + 'excerpt' => ['required', 'string'], + 'content' => ['required', 'string'], + 'status' => ['required', Rule::in(ArticleStatus::cases())], + 'thumbnail' => ['required', 'array'], + ]; + } + + public function validationAttributes(): array + { + return [ + 'title' => 'judul', + 'excerpt' => 'cuplikan', + 'content' => 'konten', + ]; + } + + public function setArticle(Article $article) + { + $this->article = $article; + + $this->title = $article->title; + $this->excerpt = $article->excerpt; + $this->content = $article->content; + $this->status = $article->status->value; + $this->thumbnail = $this->mapMediaCollection($article->getMedia('thumbnail')); + } + + public function store() + { + $this->validate(); + + DB::transaction(function () { + $article = Article::create([ + 'author_id' => auth()->id(), + 'title' => $this->title, + 'excerpt' => $this->excerpt, + 'content' => $this->content, + 'status' => $this->status, + 'published_at' => $this->status == ArticleStatus::PUBLISHED->value ? now() : null, + ]); + + $this->uploadMedia($this->thumbnail, $article, 'thumbnail'); + }); + } + + public function update() + { + $this->validate(); + + DB::transaction(function () { + $this->article->update(array_merge( + [ + 'title' => $this->title, + 'excerpt' => $this->excerpt, + 'content' => $this->content, + 'status' => $this->status, + ], + ($this->status == ArticleStatus::PUBLISHED->value && ! $this->article->published_at) + ? ['published_at' => now()] + : [] + )); + + $this->syncMedia($this->thumbnail, $this->article, 'thumbnail'); + $this->uploadMedia($this->thumbnail, $this->article, 'thumbnail'); + }); + } +} diff --git a/app/Livewire/Studio/Manage/Article/Create.php b/app/Livewire/Studio/Manage/Article/Create.php new file mode 100644 index 0000000..d1663f9 --- /dev/null +++ b/app/Livewire/Studio/Manage/Article/Create.php @@ -0,0 +1,38 @@ +canOrAbort('create article'); + + $this->form->store(); + + $this->dispatch('refreshDatatable'); + + $this->toast('Artikel berhasil ditambahkan.'); + + $this->redirectRoute('studio.manage.article.index', navigate: true); + } + + public function render() + { + return view('livewire.studio.manage.article.form', [ + 'pageTitle' => 'Tambah Artikel', + ]); + } +} diff --git a/app/Livewire/Studio/Manage/Article/Edit.php b/app/Livewire/Studio/Manage/Article/Edit.php new file mode 100644 index 0000000..1d3d598 --- /dev/null +++ b/app/Livewire/Studio/Manage/Article/Edit.php @@ -0,0 +1,44 @@ +form->setArticle($article); + } + + public function save() + { + $this->canOrAbort('update article'); + + $this->form->update(); + + $this->dispatch('refreshDatatable'); + + $this->toast('Artikel berhasil diperbarui.'); + + $this->redirectRoute('studio.manage.article.index', navigate: true); + } + + public function render() + { + return view('livewire.studio.manage.article.form', [ + 'pageTitle' => 'Ubah Artikel', + ]); + } +} diff --git a/app/Livewire/Studio/Manage/Article/Index.php b/app/Livewire/Studio/Manage/Article/Index.php new file mode 100644 index 0000000..c2837e5 --- /dev/null +++ b/app/Livewire/Studio/Manage/Article/Index.php @@ -0,0 +1,34 @@ +delete(); + + $this->dispatch('refreshDatatable'); + + $this->toast('Artikel berhasil dihapus.'); + + Flux::modals()->close(); + } + + public function render() + { + return view('livewire.studio.manage.article.index', [ + 'pageTitle' => 'Artikel', + ]); + } +} diff --git a/app/Models/Article.php b/app/Models/Article.php new file mode 100644 index 0000000..613873f --- /dev/null +++ b/app/Models/Article.php @@ -0,0 +1,42 @@ + ArticleStatus::class, + 'published_at' => 'datetime', + 'viwes' => 'int', + ]; + } + + public function getSlugOptions(): SlugOptions + { + return SlugOptions::create() + ->generateSlugsFrom('name') + ->saveSlugsTo('slug'); + } + + public function author(): BelongsTo + { + return $this->belongsTo(User::class, 'author_id'); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 7d27101..b48ac00 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -9,6 +9,7 @@ use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Foundation\Auth\User as Authenticatable; @@ -62,4 +63,9 @@ public function outlets(): BelongsToMany { return $this->belongsToMany(Outlet::class); } + + public function articles(): HasMany + { + return $this->hasMany(Article::class); + } } diff --git a/app/Providers/ViewServiceProvider.php b/app/Providers/ViewServiceProvider.php index 30bf18e..ed19a5c 100644 --- a/app/Providers/ViewServiceProvider.php +++ b/app/Providers/ViewServiceProvider.php @@ -120,6 +120,18 @@ public function boot(): void ], ], ], + [ + 'heading' => 'Kelola', + 'items' => [ + [ + 'label' => 'Artikel', + 'icon' => 'newspaper', + 'route' => 'studio.manage.article.index', + 'match' => 'studio.manage.article.*', + 'can' => 'view article', + ], + ], + ], ]; $view->with('sidebar', $sidebar); diff --git a/database/migrations/2025_10_03_025736_create_articles_table.php b/database/migrations/2025_10_03_025736_create_articles_table.php new file mode 100644 index 0000000..b5bfd99 --- /dev/null +++ b/database/migrations/2025_10_03_025736_create_articles_table.php @@ -0,0 +1,38 @@ +id(); + $table->foreignId('author_id')->constrained('users')->cascadeOnDelete(); + $table->string('title', 200); + $table->string('slug')->unique(); + $table->text('excerpt'); + $table->text('content'); + $table->enum('status', [ArticleStatus::values()])->default(ArticleStatus::PUBLISH)->comment(ArticleStatus::comment()); + $table->timestamp('published_at')->nullable(); + $table->unsignedInteger('views')->default(0); + $table->timestamp('created_at')->useCurrent(); + $table->timestamp('updated_at')->useCurrent()->useCurrentOnUpdate(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('articles'); + } +}; diff --git a/database/seeders/RolePermissionSeeder.php b/database/seeders/RolePermissionSeeder.php index fe7982b..c77678b 100644 --- a/database/seeders/RolePermissionSeeder.php +++ b/database/seeders/RolePermissionSeeder.php @@ -83,6 +83,11 @@ public function run(): void 'create expense', 'update expense', 'delete expense', + + 'view article', + 'create article', + 'update article', + 'delete article', ]; foreach ($permissions as $permission) { @@ -163,6 +168,11 @@ public function run(): void 'create bottle', 'update bottle', 'delete bottle', + + 'view article', + 'create article', + 'update article', + 'delete article', ])); $partner->syncPermissions([ diff --git a/resources/views/livewire/studio/manage/article/form.blade.php b/resources/views/livewire/studio/manage/article/form.blade.php new file mode 100644 index 0000000..8952cbb --- /dev/null +++ b/resources/views/livewire/studio/manage/article/form.blade.php @@ -0,0 +1,88 @@ + +
+
+ {{ $pageTitle }} +
+
+ + Kembali + +
+
+ +
+
+
+
+ + + Judul * + + + + + + Cuplikasn * + + + + + + Konten * + + + + +
+ +
+ + + Status * + + @foreach (\App\Enums\ArticleStatus::cases() as $status) + + {{ $status->label() }} + + @endforeach + + + + + + +
+

Thumbnail *

+
+ + @error('form.thumbnail') + + @enderror +
+
+
+
+
+ +
+ + Simpan + +
+
+
+
diff --git a/resources/views/livewire/studio/manage/article/index.blade.php b/resources/views/livewire/studio/manage/article/index.blade.php new file mode 100644 index 0000000..8464bfd --- /dev/null +++ b/resources/views/livewire/studio/manage/article/index.blade.php @@ -0,0 +1,21 @@ + +
+
+ {{ $pageTitle }} +
+ @if (auth()->user()->can('create article')) +
+ + Tambah + +
+ @endif +
+ +
+ +
+ + @include('components.confirmation.delete') +
diff --git a/routes/pages/studio.php b/routes/pages/studio.php index b40023e..ca7bec5 100644 --- a/routes/pages/studio.php +++ b/routes/pages/studio.php @@ -18,6 +18,9 @@ use App\Livewire\Studio\Loyalty\Voucher\Create as VoucherCreate; use App\Livewire\Studio\Loyalty\Voucher\Edit as VoucherEdit; use App\Livewire\Studio\Loyalty\Voucher\Index as VoucherIndex; +use App\Livewire\Studio\Manage\Article\Create as ArticleCreate; +use App\Livewire\Studio\Manage\Article\Edit as ArticleEdit; +use App\Livewire\Studio\Manage\Article\Index as ArticleIndex; use App\Livewire\Studio\Master\Outlet\Create as OutletCreate; use App\Livewire\Studio\Master\Outlet\Edit as OutletEdit; use App\Livewire\Studio\Master\Outlet\Index as OutletIndex; @@ -114,4 +117,13 @@ Route::delete('/{expense}/delete', ExpenseComponent::class)->name('delete')->middleware(); }); }); + + Route::prefix('manage')->name('studio.manage.')->group(function () { + Route::prefix('articles')->name('article.')->group(function () { + Route::get('/', ArticleIndex::class)->name('index')->middleware('can:view article'); + Route::get('/create', ArticleCreate::class)->name('create')->middleware('can:create article'); + Route::get('/{article}/edit', ArticleEdit::class)->name('edit')->middleware('can:update article'); + Route::delete('/{article}/delete', ArticleCreate::class)->name('delete')->middleware('can:delete article'); + }); + }); });