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 () {