feat(broadcast): implement broadcast management
This commit is contained in:
parent
fb424c4192
commit
8e1238a2dd
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Datatable\Studio\Information;
|
||||
|
||||
use App\Models\Broadcast;
|
||||
use App\Traits\Datatable\WithAppendColumn;
|
||||
use App\Traits\Datatable\WithConfiguration;
|
||||
use App\Traits\Datatable\WithPrependColumn;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Rappasoft\LaravelLivewireTables\DataTableComponent;
|
||||
use Rappasoft\LaravelLivewireTables\Views\Column;
|
||||
use Rappasoft\LaravelLivewireTables\Views\Columns\ArrayColumn;
|
||||
|
||||
class BroadcastsTable extends DataTableComponent
|
||||
{
|
||||
use WithAppendColumn, WithConfiguration, WithPrependColumn;
|
||||
|
||||
protected $model = Broadcast::class;
|
||||
|
||||
public function columns(): array
|
||||
{
|
||||
return [
|
||||
Column::make('Judul', 'title')->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('
|
||||
<div class="flex items-center space-x-2 bg-gray-50 border border-gray-200 rounded-lg px-2 py-1 text-xs">
|
||||
<span class="font-medium text-gray-700">{{ $value["name"] }}</span>
|
||||
</div>', ['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');
|
||||
}
|
||||
}
|
||||
85
app/Livewire/Forms/Studio/Information/BroadcastForm.php
Normal file
85
app/Livewire/Forms/Studio/Information/BroadcastForm.php
Normal file
@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Forms\Studio\Information;
|
||||
|
||||
use App\Models\Broadcast;
|
||||
use App\Traits\WithMediaHandler;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Livewire\Form;
|
||||
|
||||
class BroadcastForm extends Form
|
||||
{
|
||||
use WithMediaHandler;
|
||||
|
||||
public ?Broadcast $broadcast = null;
|
||||
|
||||
public string $title = '';
|
||||
|
||||
public string $body = '';
|
||||
|
||||
public array $role_ids = [];
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'title' => ['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);
|
||||
});
|
||||
}
|
||||
}
|
||||
106
app/Livewire/Studio/Information/Broadcast.php
Normal file
106
app/Livewire/Studio/Information/Broadcast.php
Normal file
@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Studio\Information;
|
||||
|
||||
use App\Jobs\StorePushNotification;
|
||||
use App\Livewire\Forms\Studio\Information\BroadcastForm;
|
||||
use App\Models\Broadcast as BroadcastModel;
|
||||
use App\Models\PushNotification;
|
||||
use App\Models\User;
|
||||
use App\Traits\WithAuthorization;
|
||||
use App\Traits\WithCloseModal;
|
||||
use App\Traits\WithConfirmation;
|
||||
use App\Traits\WithRoleSelector;
|
||||
use App\Traits\WithToast;
|
||||
use App\Traits\WithUpdatedData;
|
||||
use Flux\Flux;
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Attributes\Title;
|
||||
use Livewire\Component;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
#[Title('Broadcast')]
|
||||
class Broadcast extends Component
|
||||
{
|
||||
use WithAuthorization, WithCloseModal, WithConfirmation, WithRoleSelector, WithToast, WithUpdatedData;
|
||||
|
||||
public BroadcastForm $form;
|
||||
|
||||
public string $method = 'create';
|
||||
|
||||
public string $modalTitle = '';
|
||||
|
||||
public array $roles = [];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->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',
|
||||
]);
|
||||
}
|
||||
}
|
||||
23
app/Models/Broadcast.php
Normal file
23
app/Models/Broadcast.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
class Broadcast extends Model
|
||||
{
|
||||
protected $guarded = ['id'];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function audiences(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Role::class, 'broadcast_audiences', 'broadcast_id', 'audience_id');
|
||||
}
|
||||
}
|
||||
10
app/Models/BroadcastAudience.php
Normal file
10
app/Models/BroadcastAudience.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class BroadcastAudience extends Model
|
||||
{
|
||||
protected $guarded = ['id'];
|
||||
}
|
||||
@ -137,8 +137,8 @@ public function pushNotifications(): HasMany
|
||||
return $this->hasMany(PushNotification::class);
|
||||
}
|
||||
|
||||
public function users(): HasMany
|
||||
public function broadcasts(): HasMany
|
||||
{
|
||||
return $this->hasMany(User::class);
|
||||
return $this->hasMany(Broadcast::class);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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' => [
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
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('broadcasts', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
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('broadcast_audiences', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@ -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',
|
||||
|
||||
@ -0,0 +1,64 @@
|
||||
<flux:main>
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<flux:heading size="xl">{{ $pageTitle }}</flux:heading>
|
||||
</div>
|
||||
@can('create broadcast')
|
||||
<div>
|
||||
<flux:modal.trigger name="form-modal">
|
||||
<flux:button variant="primary" class="text-sm"
|
||||
wire:click="$dispatch('modal:open', {method: 'create', 'modalTitle': 'Tambah Broadcast'})">Tambah
|
||||
</flux:button>
|
||||
</flux:modal.trigger>
|
||||
</div>
|
||||
@endcan
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<livewire:datatable.studio.information.broadcasts-table />
|
||||
</div>
|
||||
|
||||
@include('components.confirmation.delete')
|
||||
<flux:modal name="form-modal" class="w-[95%] max-w-sm md:max-w-xl mx-auto" @close="closeModal('form-modal')">
|
||||
<div class="p-4 space-y-6">
|
||||
<flux:heading size="lg">{{ $modalTitle }}</flux:heading>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>Judul <span class="text-red-500 ms-1">*</span></flux:label>
|
||||
<flux:input placeholder="Yakin nggak mau coba aroma ini? 🌸✨"
|
||||
wire:model.live.debounce.500ms="form.title" autofocus autocomplete="off" />
|
||||
<flux:error name="form.title" />
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>Body <span class="text-red-500 ms-1">*</span></flux:label>
|
||||
<flux:textarea placeholder="Coba cek sekarang… siapa tahu cocok banget 😌🍃"
|
||||
wire:model.live.debounce.500ms="form.body" autocomplete="off" rows="2" />
|
||||
<flux:error name="form.body" />
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>Audiensi <span class="text-red-500 ms-1">*</span></flux:label>
|
||||
<flux:select variant="listbox" multiple searchable placeholder="Pilih Audiensi"
|
||||
wire:model.live.debounce.500ms="form.role_ids">
|
||||
<flux:select.option wire:click="selectAllRoles" wire:ignore>Pilih Semua
|
||||
</flux:select.option>
|
||||
<flux:select.option wire:click="deselectAllRoles" wire:ignore>Hapus Semua
|
||||
</flux:select.option>
|
||||
@foreach ($roles as $key => $value)
|
||||
<flux:select.option value="{{ $key }}">{{ $value }}
|
||||
</flux:select.option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
<flux:error name="form.role_ids" />
|
||||
</flux:field>
|
||||
|
||||
<div class="flex">
|
||||
<flux:spacer />
|
||||
<flux:button variant="primary" class="sm:w-auto cursor-pointer" wire:click="{{ $method }}">
|
||||
Simpan
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</flux:modal>
|
||||
</flux:main>
|
||||
@ -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 () {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user