feat(feature): implementasi CRUD fitur keunggulan parfum dan pilih kami

This commit is contained in:
Yoga Pangestu 2025-11-06 11:26:22 +07:00
parent 4f09ce32b2
commit 3030c8f6ca
15 changed files with 897 additions and 0 deletions

View File

@ -0,0 +1,97 @@
<?php
namespace App\Livewire\Datatable\Studio\Feature;
use App\Models\ChooseUs;
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 ChooseUsTable extends DataTableComponent
{
use WithConfiguration, WithPrependColumn;
protected $model = ChooseUs::class;
public function columns(): array
{
return [
Column::make('Nama', 'name')->searchable()->sortable(),
Column::make('Keterangan', 'description')->searchable()->sortable(),
Column::make('Aksi')
->label(function ($row) {
$actions = '';
$maxSortOrder = ChooseUs::max('sort_order');
if (auth()->user()->can('update choose us')) {
if ($row->sort_order > 1) {
$actions .= view('components.datatables.sort-button', [
'id' => $row->hash,
'module' => 'chooseUs',
'direction' => 'up',
'tooltip' => 'Naik',
'color' => 'cyan',
'icon' => 'arrow-up',
])->render();
$actions .= view('components.datatables.sort-button', [
'id' => $row->hash,
'module' => 'chooseUs',
'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' => 'chooseUs',
'direction' => 'down',
'tooltip' => 'Turun',
'color' => 'orange',
'icon' => 'arrow-down',
])->render();
$actions .= view('components.datatables.sort-button', [
'id' => $row->hash,
'module' => 'chooseUs',
'direction' => 'last',
'tooltip' => 'Terakhir',
'color' => 'rose',
'icon' => 'bars-arrow-down',
])->render();
}
}
if (auth()->user()->can('update choose us')) {
$actions .= view('components.datatables.edit-modal', [
'id' => $row->hash,
'method' => 'update',
'modalTitle' => 'Ubah Kategori',
])->render();
}
if (auth()->user()->can('delete choose us')) {
$actions .= view('components.datatables.delete', [
'id' => $row->hash,
'deleteRoute' => route('studio.feature.choose_us.delete', $row->hash),
])->render();
}
return $actions;
})
->html(),
];
}
public function builder(): Builder
{
return ChooseUs::select('id', 'name', 'description', 'sort_order')->orderBy('sort_order');
}
}

View File

@ -0,0 +1,97 @@
<?php
namespace App\Livewire\Datatable\Studio\Feature;
use App\Models\PerfumeFeature;
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 PerfumeFeaturesTable extends DataTableComponent
{
use WithConfiguration, WithPrependColumn;
protected $model = PerfumeFeature::class;
public function columns(): array
{
return [
Column::make('Nama', 'name')->searchable()->sortable(),
Column::make('Keterangan', 'description')->searchable()->sortable(),
Column::make('Aksi')
->label(function ($row) {
$actions = '';
$maxSortOrder = PerfumeFeature::max('sort_order');
if (auth()->user()->can('update perfume feature')) {
if ($row->sort_order > 1) {
$actions .= view('components.datatables.sort-button', [
'id' => $row->hash,
'module' => 'perfumeFeature',
'direction' => 'up',
'tooltip' => 'Naik',
'color' => 'cyan',
'icon' => 'arrow-up',
])->render();
$actions .= view('components.datatables.sort-button', [
'id' => $row->hash,
'module' => 'perfumeFeature',
'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' => 'perfumeFeature',
'direction' => 'down',
'tooltip' => 'Turun',
'color' => 'orange',
'icon' => 'arrow-down',
])->render();
$actions .= view('components.datatables.sort-button', [
'id' => $row->hash,
'module' => 'perfumeFeature',
'direction' => 'last',
'tooltip' => 'Terakhir',
'color' => 'rose',
'icon' => 'bars-arrow-down',
])->render();
}
}
if (auth()->user()->can('update perfume feature')) {
$actions .= view('components.datatables.edit-modal', [
'id' => $row->hash,
'method' => 'update',
'modalTitle' => 'Ubah Kategori',
])->render();
}
if (auth()->user()->can('delete perfume feature')) {
$actions .= view('components.datatables.delete', [
'id' => $row->hash,
'deleteRoute' => route('studio.feature.perfume_feature.delete', $row->hash),
])->render();
}
return $actions;
})
->html(),
];
}
public function builder(): Builder
{
return PerfumeFeature::select('id', 'name', 'description', 'sort_order')->orderBy('sort_order');
}
}

View File

@ -0,0 +1,123 @@
<?php
namespace App\Livewire\Forms\Studio\Feature;
use App\Models\ChooseUs;
use Livewire\Form;
class ChooseUsForm extends Form
{
public ?ChooseUs $chooseUs = null;
public string $name = '';
public string $description = '';
public string $color = '';
public string $icon = '';
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:50'],
'description' => ['required', 'string'],
'color' => ['required', 'string', 'max:20'],
'icon' => ['required', 'string', 'max:20'],
];
}
public function validationAttributes(): array
{
return [
'name' => 'nama',
'description' => 'keterangan',
'color' => 'warna',
'icon' => 'ikon',
];
}
public function setChooseUs(ChooseUs $chooseUs)
{
$this->chooseUs = $chooseUs;
$this->name = $chooseUs->name;
$this->description = $chooseUs->description;
$this->color = $chooseUs->color;
$this->icon = $chooseUs->icon;
}
public function store()
{
$this->validate();
ChooseUs::create([
'name' => $this->name,
'description' => $this->description,
'color' => $this->color,
'icon' => $this->icon,
'sort_order' => ChooseUs::count() + 1,
]);
}
public function update()
{
$this->validate();
$this->chooseUs->update([
'name' => $this->name,
'description' => $this->description,
'color' => $this->color,
'icon' => $this->icon,
]);
}
public function delete()
{
$this->chooseUs->delete();
ChooseUs::where('sort_order', '>', $this->chooseUs->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 = ChooseUs::where('sort_order', '<', $this->chooseUs->sort_order)
->orderByDesc('sort_order')
->first();
} else {
$swap = ChooseUs::where('sort_order', '>', $this->chooseUs->sort_order)
->orderBy('sort_order')
->first();
}
if ($swap) {
$temp = $this->chooseUs->sort_order;
$this->chooseUs->update(['sort_order' => $swap->sort_order]);
$swap->update(['sort_order' => $temp]);
}
} elseif ($direction === 'first') {
$this->chooseUs->update(['sort_order' => ChooseUs::min('sort_order') - 1]);
$this->normalizeSortOrder();
} elseif ($direction === 'last') {
$this->chooseUs->update(['sort_order' => ChooseUs::max('sort_order') + 1]);
$this->normalizeSortOrder();
}
}
protected function normalizeSortOrder()
{
$chooseUss = ChooseUs::orderBy('sort_order')->get();
foreach ($chooseUss as $index => $chooseUs) {
$chooseUs->update(['sort_order' => $index + 1]);
}
}
}

View File

@ -0,0 +1,109 @@
<?php
namespace App\Livewire\Forms\Studio\Feature;
use App\Models\PerfumeFeature;
use Livewire\Form;
class PerfumeFeatureForm extends Form
{
public ?PerfumeFeature $perfumeFeature = null;
public string $name = '';
public string $description = '';
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:50'],
'description' => ['required', 'string'],
];
}
public function validationAttributes(): array
{
return [
'name' => 'nama',
'description' => 'keterangan',
];
}
public function setPerfumeFeature(PerfumeFeature $perfumeFeature)
{
$this->perfumeFeature = $perfumeFeature;
$this->name = $perfumeFeature->name;
$this->description = $perfumeFeature->description;
}
public function store()
{
$this->validate();
PerfumeFeature::create([
'name' => $this->name,
'description' => $this->description,
'sort_order' => PerfumeFeature::count() + 1,
]);
}
public function update()
{
$this->validate();
$this->perfumeFeature->update([
'name' => $this->name,
'description' => $this->description,
]);
}
public function delete()
{
$this->perfumeFeature->delete();
PerfumeFeature::where('sort_order', '>', $this->perfumeFeature->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 = PerfumeFeature::where('sort_order', '<', $this->perfumeFeature->sort_order)
->orderByDesc('sort_order')
->first();
} else {
$swap = PerfumeFeature::where('sort_order', '>', $this->perfumeFeature->sort_order)
->orderBy('sort_order')
->first();
}
if ($swap) {
$temp = $this->perfumeFeature->sort_order;
$this->perfumeFeature->update(['sort_order' => $swap->sort_order]);
$swap->update(['sort_order' => $temp]);
}
} elseif ($direction === 'first') {
$this->perfumeFeature->update(['sort_order' => PerfumeFeature::min('sort_order') - 1]);
$this->normalizeSortOrder();
} elseif ($direction === 'last') {
$this->perfumeFeature->update(['sort_order' => PerfumeFeature::max('sort_order') + 1]);
$this->normalizeSortOrder();
}
}
protected function normalizeSortOrder()
{
$perfumeFeatures = PerfumeFeature::orderBy('sort_order')->get();
foreach ($perfumeFeatures as $index => $perfumeFeature) {
$perfumeFeature->update(['sort_order' => $index + 1]);
}
}
}

View File

@ -0,0 +1,99 @@
<?php
namespace App\Livewire\Studio\Feature;
use App\Livewire\Forms\Studio\Feature\ChooseUsForm;
use App\Models\ChooseUs as ChooseUsModel;
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('Pilih Kami')]
class ChooseUs extends Component
{
use WithAuthorization, WithCloseModal, WithConfirmation, WithToast, WithUpdatedData;
public ChooseUsForm $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->setChooseUs(ChooseUsModel::byHashOrFail($id));
}
}
public function create()
{
$this->canOrAbort('create choose us');
$this->form->store();
$this->dispatch('refreshDatatable');
$this->toast('Pilih kami berhasil ditambahkan.');
Flux::modals()->close();
}
public function update()
{
$this->canOrAbort('update choose us');
$this->form->update();
$this->dispatch('refreshDatatable');
$this->toast('Pilih kami berhasil diperbarui.');
Flux::modals()->close();
}
public function delete(ChooseUsModel $chooseUs)
{
$this->form->chooseUs = $chooseUs;
$this->form->delete();
$this->dispatch('refreshDatatable');
$this->toast('Pilih kami berhasil dihapus.');
Flux::modals()->close();
}
#[On('fn:sortOrder')]
public function sortOrder(ChooseUsModel $chooseUs, string $direction)
{
$this->canOrAbort('update choose us');
$this->form->chooseUs = $chooseUs;
$this->form->sortOrder($direction);
$this->dispatch('refreshDatatable');
}
public function render()
{
return view('livewire.studio.feature.choose-us', [
'pageTitle' => 'Pilih Kami',
]);
}
}

View File

@ -0,0 +1,99 @@
<?php
namespace App\Livewire\Studio\Feature;
use App\Livewire\Forms\Studio\Feature\PerfumeFeatureForm;
use App\Models\PerfumeFeature as PerfumeFeatureModel;
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('Keunggulan Parfum')]
class PerfumeFeature extends Component
{
use WithAuthorization, WithCloseModal, WithConfirmation, WithToast, WithUpdatedData;
public PerfumeFeatureForm $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->setPerfumeFeature(PerfumeFeatureModel::byHashOrFail($id));
}
}
public function create()
{
$this->canOrAbort('create perfume feature');
$this->form->store();
$this->dispatch('refreshDatatable');
$this->toast('Keunggulan parfum berhasil ditambahkan.');
Flux::modals()->close();
}
public function update()
{
$this->canOrAbort('update perfume feature');
$this->form->update();
$this->dispatch('refreshDatatable');
$this->toast('Keunggulan parfum berhasil diperbarui.');
Flux::modals()->close();
}
public function delete(PerfumeFeatureModel $perfumeFeature)
{
$this->form->perfumeFeature = $perfumeFeature;
$this->form->delete();
$this->dispatch('refreshDatatable');
$this->toast('Keunggulan parfum berhasil dihapus.');
Flux::modals()->close();
}
#[On('fn:sortOrder')]
public function sortOrder(PerfumeFeatureModel $perfumeFeature, string $direction)
{
$this->canOrAbort('update perfume feature');
$this->form->perfumeFeature = $perfumeFeature;
$this->form->sortOrder($direction);
$this->dispatch('refreshDatatable');
}
public function render()
{
return view('livewire.studio.feature.perfume-features', [
'pageTitle' => 'Keunggulan Parfum',
]);
}
}

21
app/Models/ChooseUs.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 ChooseUs extends Model
{
use HashableId, SoftDeletes;
protected $guarded = ['id'];
protected function casts(): array
{
return [
'sort_order' => 'int',
];
}
}

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 PerfumeFeature extends Model
{
use HashableId, SoftDeletes;
protected $guarded = ['id'];
protected function casts(): array
{
return [
'sort_order' => 'int',
];
}
}

View File

@ -167,6 +167,25 @@ public function boot(): void
],
],
],
[
'heading' => 'Keunggulan',
'items' => [
[
'label' => 'Keunggulan Parfum',
'icon' => 'rocket-launch',
'route' => 'studio.feature.perfume_feature.index',
'match' => 'studio.feature.perfume_feature.*',
'can' => 'view perfume feature',
],
[
'label' => 'Pilih Kami',
'icon' => 'trophy',
'route' => 'studio.feature.choose_us.index',
'match' => 'studio.feature.choose_us.*',
'can' => 'view choose us',
],
],
],
[
'heading' => 'Pengaturan',
'items' => [

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('perfume_features', function (Blueprint $table) {
$table->id();
$table->string('name', 50);
$table->text('description');
$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('perfume_features');
}
};

View File

@ -0,0 +1,34 @@
<?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('choose_us', function (Blueprint $table) {
$table->id();
$table->string('name', 50);
$table->text('description');
$table->string('icon', 20);
$table->string('color', 20);
$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('choose_us');
}
};

View File

@ -120,6 +120,16 @@ public function run(): void
'update faq',
'delete faq',
'view perfume feature',
'create perfume feature',
'update perfume feature',
'delete perfume feature',
'view choose us',
'create choose us',
'update choose us',
'delete choose us',
'view application settings',
'view whatsapp settings',

View File

@ -0,0 +1,66 @@
<flux:main>
<div class="flex justify-between items-center">
<div>
<flux:heading size="xl">{{ $pageTitle }}</flux:heading>
</div>
@can('create choose us')
<div>
<flux:modal.trigger name="form-modal">
<flux:button variant="primary" class="text-sm"
wire:click="$dispatch('modal:open', {method: 'create', 'modalTitle': 'Tambah Pilih Kami'})">
Tambah
</flux:button>
</flux:modal.trigger>
</div>
@endcan
</div>
<div class="mt-6">
<livewire:datatable.studio.feature.choose-us-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>Nama <span class="text-red-500 ms-1">*</span></flux:label>
<flux:input placeholder="200+ Aroma" wire:model.live.debounce.500ms="form.name" autofocus
autocomplete="off" />
<flux:error name="form.name" />
</flux:field>
<flux:field>
<flux:label>Keterangan <span class="text-red-500 ms-1">*</span></flux:label>
<flux:textarea wire:model="form.description" placeholder="Kami punya 200 lebih aroma ..."
auotocomplete="off" />
</flux:field>
<div class="lg:col-span-2">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<flux:field>
<flux:label>Warna <span class="text-red-500 ms-1">*</span></flux:label>
<flux:input type="color" wire:model="form.color" autofocus
autocomplete="off" />
<flux:error name="form.color" />
</flux:field>
<flux:field>
<flux:label>Icon <span class="text-red-500 ms-1">*</span></flux:label>
<flux:input type="text" wire:model="form.icon" autofocus
autocomplete="off" />
<flux:error name="form.icon" />
</flux:field>
</div>
</div>
<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

@ -0,0 +1,56 @@
<flux:main>
<div class="flex justify-between items-center">
<div>
<flux:heading size="xl">{{ $pageTitle }}</flux:heading>
</div>
@can('create perfume feature')
<div>
<flux:modal.trigger name="form-modal">
<flux:button variant="primary" class="text-sm"
wire:click="$dispatch('modal:open', {method: 'create', 'modalTitle': 'Tambah Keunggulan Parfum'})">
Tambah
</flux:button>
</flux:modal.trigger>
</div>
@endcan
</div>
<div class="mt-6">
<flux:callout icon="megaphone" color="blue" class="mb-6">
<flux:callout.heading>Informasi</flux:callout.heading>
<flux:callout.text>
Untuk tampilan yang lebih bagus dan rapi, disarankan hanya mengisi 4 data saja.
</flux:callout.text>
</flux:callout>
<livewire:datatable.studio.feature.perfume-features-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>Nama <span class="text-red-500 ms-1">*</span></flux:label>
<flux:input placeholder="Tahan Lama" wire:model.live.debounce.500ms="form.name" autofocus
autocomplete="off" />
<flux:error name="form.name" />
</flux:field>
<flux:field>
<flux:label>Keterangan <span class="text-red-500 ms-1">*</span></flux:label>
<flux:textarea wire:model="form.description"
placeholder="Parfum kami diracik secara profesional sehingga memiliki kualitas terbaik."
auotocomplete="off" />
</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

@ -16,6 +16,8 @@
use App\Livewire\Studio\Catalog\Product\Edit as ProductEdit;
use App\Livewire\Studio\Catalog\Product\Index as ProductIndex;
use App\Livewire\Studio\Dashboard\Overview as OverviewComponent;
use App\Livewire\Studio\Feature\ChooseUs as ChooseUsComponent;
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\Loyalty\Customer as CustomerComponent;
@ -174,6 +176,18 @@
});
});
Route::prefix('feature')->name('studio.feature.')->group(function () {
Route::prefix('perfume-features')->name('perfume_feature.')->group(function () {
Route::get('/', PerfumeFeatureComponent::class)->name('index')->middleware('can:view perfume feature');
Route::delete('/{perfume_feature}/delete', PerfumeFeatureComponent::class)->name('delete')->middleware('can:delete perfume feature');
});
Route::prefix('choose-us')->name('choose_us.')->group(function () {
Route::get('/', ChooseUsComponent::class)->name('index')->middleware('can:view choose us');
Route::delete('/{choose_us}/delete', ChooseUsComponent::class)->name('delete')->middleware('can:delete choose us');
});
});
Route::get('/settings/applications', ApplicationComponent::class)->name('studio.setting.application')->middleware('can:view application settings');
Route::get('/settings/whatsapp', WhatsappComponent::class)->name('studio.setting.whatsapp')->middleware('can:view whatsapp settings');
});