From 8e1238a2ddeebe29596d2e0f6e05629cec4e1fef Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Fri, 21 Nov 2025 21:58:24 +0700 Subject: [PATCH] feat(broadcast): implement broadcast management --- .../Studio/Information/BroadcastsTable.php | 50 +++++++++ .../Studio/Information/BroadcastForm.php | 85 ++++++++++++++ app/Livewire/Studio/Information/Broadcast.php | 106 ++++++++++++++++++ app/Models/Broadcast.php | 23 ++++ app/Models/BroadcastAudience.php | 10 ++ app/Models/User.php | 4 +- app/Providers/ViewServiceProvider.php | 12 ++ ...5_11_21_151835_create_broadcasts_table.php | 31 +++++ ...91717_create_broadcast_audiences_table.php | 29 +++++ database/seeders/RolePermissionSeeder.php | 3 + .../studio/information/broadcasts.blade.php | 64 +++++++++++ routes/pages/studio.php | 15 ++- 12 files changed, 425 insertions(+), 7 deletions(-) create mode 100644 app/Livewire/Datatable/Studio/Information/BroadcastsTable.php create mode 100644 app/Livewire/Forms/Studio/Information/BroadcastForm.php create mode 100644 app/Livewire/Studio/Information/Broadcast.php create mode 100644 app/Models/Broadcast.php create mode 100644 app/Models/BroadcastAudience.php create mode 100644 database/migrations/2025_11_21_151835_create_broadcasts_table.php create mode 100644 database/migrations/2025_11_21_191717_create_broadcast_audiences_table.php create mode 100644 resources/views/livewire/studio/information/broadcasts.blade.php diff --git a/app/Livewire/Datatable/Studio/Information/BroadcastsTable.php b/app/Livewire/Datatable/Studio/Information/BroadcastsTable.php new file mode 100644 index 0000000..08bc8e1 --- /dev/null +++ b/app/Livewire/Datatable/Studio/Information/BroadcastsTable.php @@ -0,0 +1,50 @@ +searchable(), + + Column::make('Body', 'body')->searchable(), + + ArrayColumn::make('Audiensi') + ->data( + fn ($value, $row) => $row->audiences->map(fn ($audience) => [ + 'name' => $audience->name, + ])->toArray() + ) + ->outputFormat(fn ($index, $value) => Blade::render(' +
+ {{ $value["name"] }} +
', ['value' => $value])) + ->flexRow(['class' => 'gap-2 flex-wrap']), + + Column::make('Tanggal') + ->label(fn ($row, $column) => formatDateTime($row->created_at)), + ]; + } + + public function builder(): Builder + { + return Broadcast::select('id', 'title', 'body') + ->with('audiences'); + } +} diff --git a/app/Livewire/Forms/Studio/Information/BroadcastForm.php b/app/Livewire/Forms/Studio/Information/BroadcastForm.php new file mode 100644 index 0000000..3236c59 --- /dev/null +++ b/app/Livewire/Forms/Studio/Information/BroadcastForm.php @@ -0,0 +1,85 @@ + ['required', 'string', 'max:30'], + 'body' => ['required', 'string', 'max:50'], + 'role_ids' => ['required', 'array', 'min:1'], + 'role_ids.*' => Rule::exists('roles', 'id'), + ]; + } + + public function validationAttributes(): array + { + return [ + 'title' => 'judul', + 'role_ids' => 'audiensi', + 'role_ids.*' => 'audiensi', + ]; + } + + public function setBroadcast(Broadcast $broadcast) + { + $this->title = $broadcast->title; + $this->body = $broadcast->body; + $this->role_ids = $broadcast->roles->pluck('id')->toArray(); + } + + public function store() + { + $this->validate(); + + DB::transaction(function () use (&$broadcast) { + $broadcast = Broadcast::create([ + 'user_id' => auth()->id(), + 'title' => $this->title, + 'body' => $this->body, + ]); + + $broadcast->audiences()->attach($this->role_ids); + }); + + return [ + 'status' => true, + 'data' => [ + 'title' => $broadcast->title, + 'body' => $broadcast->body, + 'audience_ids' => $this->role_ids, + ], + ]; + } + + public function update() + { + $this->validate(); + + DB::transaction(function () { + $this->broadcast->update([ + 'title' => $this->title, + ]); + + $this->broadcast->audiences()->sync($this->role_ids); + }); + } +} diff --git a/app/Livewire/Studio/Information/Broadcast.php b/app/Livewire/Studio/Information/Broadcast.php new file mode 100644 index 0000000..4d4056f --- /dev/null +++ b/app/Livewire/Studio/Information/Broadcast.php @@ -0,0 +1,106 @@ +roles = Role::where('name', '!=', 'Developer')->orderBy('id')->pluck('name', 'id')->toArray(); + } + + #[On('modal:open')] + public function openModal(string $method, string $modalTitle, ?string $id = null) + { + $this->resetValidation(); + $this->resetErrorBag(); + + $this->method = $method; + $this->modalTitle = $modalTitle; + + if ($id) { + $this->form->setBroadcast(BroadcastModel::byHashOrFail($id)); + } + } + + public function create() + { + $this->canOrAbort('create broadcast'); + + $result = $this->form->store(); + + $userIds = User::role(array_map('intval', $result['data']['audience_ids']))->pluck('id')->toArray(); + StorePushNotification::dispatch( + PushNotification::whereIn('user_id', $userIds)->get(), + [ + 'title' => $result['data']['title'], + 'body' => $result['data']['body'], + ], + ); + + $this->dispatch('refreshDatatable'); + + $this->toast('Broadcast berhasil ditambahkan.'); + + Flux::modals()->close(); + } + + public function update() + { + $this->canOrAbort('update broadcast'); + + $this->form->update(); + + $this->dispatch('refreshDatatable'); + + $this->toast('Broadcast berhasil diperbarui.'); + + Flux::modals()->close(); + } + + public function delete(BroadcastModel $broadcast) + { + $broadcast->delete(); + + $this->dispatch('refreshDatatable'); + + $this->toast('Broadcast berhasil dihapus.'); + + Flux::modals()->close(); + } + + public function render() + { + return view('livewire.studio.information.broadcasts', [ + 'pageTitle' => 'Broadcast', + ]); + } +} diff --git a/app/Models/Broadcast.php b/app/Models/Broadcast.php new file mode 100644 index 0000000..5d3124a --- /dev/null +++ b/app/Models/Broadcast.php @@ -0,0 +1,23 @@ +belongsTo(User::class); + } + + public function audiences(): BelongsToMany + { + return $this->belongsToMany(Role::class, 'broadcast_audiences', 'broadcast_id', 'audience_id'); + } +} diff --git a/app/Models/BroadcastAudience.php b/app/Models/BroadcastAudience.php new file mode 100644 index 0000000..3df9c74 --- /dev/null +++ b/app/Models/BroadcastAudience.php @@ -0,0 +1,10 @@ +hasMany(PushNotification::class); } - public function users(): HasMany + public function broadcasts(): HasMany { - return $this->hasMany(User::class); + return $this->hasMany(Broadcast::class); } } diff --git a/app/Providers/ViewServiceProvider.php b/app/Providers/ViewServiceProvider.php index bf92c5b..f2f4866 100644 --- a/app/Providers/ViewServiceProvider.php +++ b/app/Providers/ViewServiceProvider.php @@ -186,6 +186,18 @@ public function boot(): void ], ], ], + [ + 'heading' => 'Informasi', + 'items' => [ + [ + 'label' => 'Broadcast', + 'icon' => 'megaphone', + 'route' => 'studio.information.broadcast.index', + 'match' => 'studio.information.broadcast.*', + 'can' => 'view broadcast', + ], + ], + ], [ 'heading' => 'Keunggulan', 'items' => [ diff --git a/database/migrations/2025_11_21_151835_create_broadcasts_table.php b/database/migrations/2025_11_21_151835_create_broadcasts_table.php new file mode 100644 index 0000000..5c930da --- /dev/null +++ b/database/migrations/2025_11_21_151835_create_broadcasts_table.php @@ -0,0 +1,31 @@ +id(); + $table->foreignId('user_id')->constrained()->cascadeOnDelete(); + $table->string('title', 30); + $table->string('body', 50); + $table->timestamp('created_at')->useCurrent(); + $table->timestamp('updated_at')->nullable()->useCurrentOnUpdate(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('broadcasts'); + } +}; diff --git a/database/migrations/2025_11_21_191717_create_broadcast_audiences_table.php b/database/migrations/2025_11_21_191717_create_broadcast_audiences_table.php new file mode 100644 index 0000000..5a13d11 --- /dev/null +++ b/database/migrations/2025_11_21_191717_create_broadcast_audiences_table.php @@ -0,0 +1,29 @@ +id(); + $table->foreignId('broadcast_id')->constrained()->cascadeOnDelete(); + $table->foreignId('audience_id')->constrained('roles')->cascadeOnDelete(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('broadcast_audiences'); + } +}; diff --git a/database/seeders/RolePermissionSeeder.php b/database/seeders/RolePermissionSeeder.php index 9b0ebfa..5dfd1ce 100644 --- a/database/seeders/RolePermissionSeeder.php +++ b/database/seeders/RolePermissionSeeder.php @@ -127,6 +127,9 @@ public function run(): void 'update faq', 'delete faq', + 'view broadcast', + 'create broadcast', + 'view perfume feature', 'create perfume feature', 'update perfume feature', diff --git a/resources/views/livewire/studio/information/broadcasts.blade.php b/resources/views/livewire/studio/information/broadcasts.blade.php new file mode 100644 index 0000000..9d48a38 --- /dev/null +++ b/resources/views/livewire/studio/information/broadcasts.blade.php @@ -0,0 +1,64 @@ + +
+
+ {{ $pageTitle }} +
+ @can('create broadcast') +
+ + Tambah + + +
+ @endcan +
+ +
+ +
+ + @include('components.confirmation.delete') + +
+ {{ $modalTitle }} + + + Judul * + + + + + + Body * + + + + + + Audiensi * + + Pilih Semua + + Hapus Semua + + @foreach ($roles as $key => $value) + {{ $value }} + + @endforeach + + + + +
+ + + Simpan + +
+
+
+
diff --git a/routes/pages/studio.php b/routes/pages/studio.php index ec0f33d..007a8a2 100644 --- a/routes/pages/studio.php +++ b/routes/pages/studio.php @@ -21,6 +21,7 @@ use App\Livewire\Studio\Feature\PerfumeFeature as PerfumeFeatureComponent; use App\Livewire\Studio\Finance\Expense as ExpenseComponent; use App\Livewire\Studio\Finance\Payroll as PayrollComponent; +use App\Livewire\Studio\Information\Broadcast as BroadcastComponent; use App\Livewire\Studio\Loyalty\Customer as CustomerComponent; use App\Livewire\Studio\Loyalty\Tier as TierComponent; use App\Livewire\Studio\Loyalty\Voucher\Create as VoucherCreate; @@ -173,16 +174,20 @@ Route::delete('/{article}/delete', ArticleCreate::class)->name('delete')->middleware('can:delete article'); }); - Route::prefix('faqs')->name('faq.')->group(function () { - Route::get('/', FaqComponent::class)->name('index')->middleware('can:view faq'); - Route::delete('/{faq}/delete', FaqComponent::class)->name('delete')->middleware('can:delete faq'); - }); - Route::prefix('price-requests')->name('price_request.')->group(function () { Route::get('/', PriceRequestComponent::class)->name('index')->middleware('can:view price request'); Route::patch('/{priceRequest}/approve', PriceRequestComponent::class)->name('approve')->middleware('can:approve price request'); Route::patch('/{priceRequest}/reject', PriceRequestComponent::class)->name('reject')->middleware('can:reject price request'); }); + + Route::prefix('faqs')->name('faq.')->group(function () { + Route::get('/', FaqComponent::class)->name('index')->middleware('can:view faq'); + Route::delete('/{faq}/delete', FaqComponent::class)->name('delete')->middleware('can:delete faq'); + }); + }); + + Route::prefix('information')->name('studio.information.')->group(function () { + Route::get('/broadcasts', BroadcastComponent::class)->name('broadcast.index')->middleware('can:view broadcast'); }); Route::prefix('feature')->name('studio.feature.')->group(function () {