feat(faq): implementasi CRUD

This commit is contained in:
Yoga Pangestu 2025-11-06 09:00:15 +07:00
parent 182cb1fda0
commit 4f09ce32b2
9 changed files with 424 additions and 3 deletions

View File

@ -0,0 +1,95 @@
<?php
namespace App\Livewire\Datatable\Studio\Manage;
use App\Models\Faq;
use App\Traits\Datatable\WithConfiguration;
use App\Traits\Datatable\WithPrependColumn;
use Illuminate\Database\Eloquent\Builder;
use Rappasoft\LaravelLivewireTables\DataTableComponent;
use Rappasoft\LaravelLivewireTables\Views\Column;
class FaqsTable extends DataTableComponent
{
use WithConfiguration, WithPrependColumn;
protected $model = Faq::class;
public function columns(): array
{
return [
Column::make('Pertanyaan', 'question')->searchable(),
Column::make('Aksi')
->label(function ($row) {
$actions = '';
$maxSortOrder = Faq::max('sort_order');
if (auth()->user()->can('update faq')) {
if ($row->sort_order > 1) {
$actions .= view('components.datatables.sort-button', [
'id' => $row->hash,
'module' => 'faq',
'direction' => 'up',
'tooltip' => 'Naik',
'color' => 'cyan',
'icon' => 'arrow-up',
])->render();
$actions .= view('components.datatables.sort-button', [
'id' => $row->hash,
'module' => 'faq',
'direction' => 'first',
'tooltip' => 'Pertama',
'color' => 'emerald',
'icon' => 'bars-arrow-up',
])->render();
}
if ($row->sort_order < $maxSortOrder) {
$actions .= view('components.datatables.sort-button', [
'id' => $row->hash,
'module' => 'faq',
'direction' => 'down',
'tooltip' => 'Turun',
'color' => 'orange',
'icon' => 'arrow-down',
])->render();
$actions .= view('components.datatables.sort-button', [
'id' => $row->hash,
'module' => 'faq',
'direction' => 'last',
'tooltip' => 'Terakhir',
'color' => 'rose',
'icon' => 'bars-arrow-down',
])->render();
}
}
if (auth()->user()->can('update faq')) {
$actions .= view('components.datatables.edit-modal', [
'id' => $row->hash,
'method' => 'update',
'modalTitle' => 'Ubah Kategori',
])->render();
}
if (auth()->user()->can('delete faq')) {
$actions .= view('components.datatables.delete', [
'id' => $row->hash,
'deleteRoute' => route('studio.manage.faq.delete', $row->hash),
])->render();
}
return $actions;
})
->html(),
];
}
public function builder(): Builder
{
return Faq::select('id', 'question', 'sort_order')->orderBy('sort_order');
}
}

View File

@ -0,0 +1,109 @@
<?php
namespace App\Livewire\Forms\Studio\Manage;
use App\Models\Faq;
use Livewire\Form;
class FaqForm extends Form
{
public ?Faq $faq = null;
public string $question = '';
public string $answer = '';
public function rules(): array
{
return [
'question' => ['required', 'string', 'max:100'],
'answer' => ['required', 'string'],
];
}
public function validationAttributes(): array
{
return [
'question' => 'pertanyaan',
'answer' => 'jawaban',
];
}
public function setFaq(Faq $faq)
{
$this->faq = $faq;
$this->question = $faq->question;
$this->answer = $faq->answer;
}
public function store()
{
$this->validate();
Faq::create([
'question' => $this->question,
'answer' => $this->answer,
'sort_order' => Faq::count() + 1,
]);
}
public function update()
{
$this->validate();
$this->faq->update([
'question' => $this->question,
'answer' => $this->answer,
]);
}
public function delete()
{
$this->faq->delete();
Faq::where('sort_order', '>', $this->faq->sort_order)
->orderBy('sort_order')
->get()
->each(function ($cat) {
$cat->update(['sort_order' => $cat->sort_order - 1]);
});
}
public function sortOrder(string $direction)
{
if (in_array($direction, ['up', 'down'])) {
$swap = null;
if ($direction === 'up') {
$swap = Faq::where('sort_order', '<', $this->faq->sort_order)
->orderByDesc('sort_order')
->first();
} else {
$swap = Faq::where('sort_order', '>', $this->faq->sort_order)
->orderBy('sort_order')
->first();
}
if ($swap) {
$temp = $this->faq->sort_order;
$this->faq->update(['sort_order' => $swap->sort_order]);
$swap->update(['sort_order' => $temp]);
}
} elseif ($direction === 'first') {
$this->faq->update(['sort_order' => Faq::min('sort_order') - 1]);
$this->normalizeSortOrder();
} elseif ($direction === 'last') {
$this->faq->update(['sort_order' => Faq::max('sort_order') + 1]);
$this->normalizeSortOrder();
}
}
protected function normalizeSortOrder()
{
$categories = Faq::orderBy('sort_order')->get();
foreach ($categories as $index => $faq) {
$faq->update(['sort_order' => $index + 1]);
}
}
}

View File

@ -0,0 +1,99 @@
<?php
namespace App\Livewire\Studio\Manage;
use App\Livewire\Forms\Studio\Manage\FaqForm;
use App\Models\Faq as FaqModel;
use App\Traits\WithAuthorization;
use App\Traits\WithCloseModal;
use App\Traits\WithConfirmation;
use App\Traits\WithToast;
use App\Traits\WithUpdatedData;
use Flux\Flux;
use Livewire\Attributes\On;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Title('FAQs')]
class Faq extends Component
{
use WithAuthorization, WithCloseModal, WithConfirmation, WithToast, WithUpdatedData;
public FaqForm $form;
public string $method = 'create';
public string $modalTitle = '';
#[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->setFaq(FaqModel::byHashOrFail($id));
}
}
public function create()
{
$this->canOrAbort('create faq');
$this->form->store();
$this->dispatch('refreshDatatable');
$this->toast('FAQs berhasil ditambahkan.');
Flux::modals()->close();
}
public function update()
{
$this->canOrAbort('update faq');
$this->form->update();
$this->dispatch('refreshDatatable');
$this->toast('FAQs berhasil diperbarui.');
Flux::modals()->close();
}
public function delete(FaqModel $faq)
{
$this->form->faq = $faq;
$this->form->delete();
$this->dispatch('refreshDatatable');
$this->toast('FAQs berhasil dihapus.');
Flux::modals()->close();
}
#[On('fn:sortOrder')]
public function sortOrder(FaqModel $faq, string $direction)
{
$this->canOrAbort('update faq');
$this->form->faq = $faq;
$this->form->sortOrder($direction);
$this->dispatch('refreshDatatable');
}
public function render()
{
return view('livewire.studio.manage.faqs', [
'pageTitle' => 'FAQs',
]);
}
}

21
app/Models/Faq.php Normal file
View File

@ -0,0 +1,21 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Veelasky\LaravelHashId\Eloquent\HashableId;
class Faq extends Model
{
use HashableId, SoftDeletes;
protected $guarded = ['id'];
protected function casts(): array
{
return [
'sort_order' => 'int',
];
}
}

View File

@ -158,6 +158,13 @@ public function boot(): void
'match' => 'studio.manage.article.*',
'can' => 'view article',
],
[
'label' => 'FAQs',
'icon' => 'question-mark-circle',
'route' => 'studio.manage.faq.index',
'match' => 'studio.manage.faq.*',
'can' => 'view faq',
],
],
],
[

View File

@ -0,0 +1,32 @@
<?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('faqs', function (Blueprint $table) {
$table->id();
$table->string('question', 100);
$table->text('answer');
$table->unsignedInteger('sort_order');
$table->timestamp('created_at')->useCurrent();
$table->timestamp('updated_at')->useCurrent()->useCurrentOnUpdate();
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('faqs');
}
};

View File

@ -106,14 +106,19 @@ public function run(): void
'delete order',
'show order',
'view purchase',
'create purchase',
'delete purchase',
'view article',
'create article',
'update article',
'delete article',
'view purchase',
'create purchase',
'delete purchase',
'view faq',
'create faq',
'update faq',
'delete faq',
'view application settings',

View File

@ -0,0 +1,47 @@
<flux:main>
<div class="flex justify-between items-center">
<div>
<flux:heading size="xl">{{ $pageTitle }}</flux:heading>
</div>
@can('create faq')
<div>
<flux:modal.trigger name="form-modal">
<flux:button variant="primary" class="text-sm"
wire:click="$dispatch('modal:open', {method: 'create', 'modalTitle': 'Tambah FAQs'})">Tambah
</flux:button>
</flux:modal.trigger>
</div>
@endcan
</div>
<div class="mt-6">
<livewire:datatable.studio.manage.faqs-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>Pertanyaan <span class="text-red-500 ms-1">*</span></flux:label>
<flux:input placeholder="Bagaimana Cara Jadi Member?" wire:model.live.debounce.500ms="form.question"
autofocus autocomplete="off" />
<flux:error name="form.question" />
</flux:field>
<flux:field>
<flux:label>Jawaban <span class="text-red-500 ms-1">*</span></flux:label>
<flux:editor wire:model="form.answer" placeholder="1. Silakan Anda mendaftar terlebih dahulu."
auotocomplete="off" class="**:data-[slot=content]:min-h-[100px]!" />
</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>

View File

@ -26,6 +26,7 @@
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\Manage\Faq as FaqComponent;
use App\Livewire\Studio\Manage\Order\Create as OrderCreate;
use App\Livewire\Studio\Manage\Order\Index as OrderIndex;
use App\Livewire\Studio\Manage\Order\Show as OrderShow;
@ -166,6 +167,11 @@
Route::get('/{article}/edit', ArticleEdit::class)->name('edit')->middleware('can:update article');
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::get('/settings/applications', ApplicationComponent::class)->name('studio.setting.application')->middleware('can:view application settings');