feat(brand): implementasi crud fitur merek
This commit is contained in:
parent
54d56afa00
commit
fc6b9fddb2
89
app/Livewire/Datatable/BrandsTable.php
Normal file
89
app/Livewire/Datatable/BrandsTable.php
Normal file
@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Datatable;
|
||||
|
||||
use App\Models\Brand;
|
||||
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 BrandsTable extends DataTableComponent
|
||||
{
|
||||
use WithConfiguration, WithPrependColumn;
|
||||
|
||||
protected $model = Brand::class;
|
||||
|
||||
public function columns(): array
|
||||
{
|
||||
return [
|
||||
Column::make('Nama', 'name')->searchable(),
|
||||
|
||||
Column::make('Aksi')
|
||||
->label(function ($row) {
|
||||
$actions = '';
|
||||
$maxSortOrder = Brand::max('sort_order');
|
||||
|
||||
if ($row->sort_order > 1) {
|
||||
$actions .= view('components.datatables.sort-button', [
|
||||
'id' => $row->id,
|
||||
'module' => 'brand',
|
||||
'direction' => 'up',
|
||||
'tooltip' => 'Naik',
|
||||
'color' => 'cyan',
|
||||
'icon' => 'arrow-up',
|
||||
])->render();
|
||||
|
||||
$actions .= view('components.datatables.sort-button', [
|
||||
'id' => $row->id,
|
||||
'module' => 'brand',
|
||||
'direction' => 'first',
|
||||
'tooltip' => 'Pertama',
|
||||
'color' => 'emerald',
|
||||
'icon' => 'bars-arrow-up',
|
||||
])->render();
|
||||
}
|
||||
|
||||
if ($row->sort_order < $maxSortOrder) {
|
||||
$actions .= view('components.datatables.sort-button', [
|
||||
'id' => $row->id,
|
||||
'module' => 'brand',
|
||||
'direction' => 'down',
|
||||
'tooltip' => 'Turun',
|
||||
'color' => 'orange',
|
||||
'icon' => 'arrow-down',
|
||||
])->render();
|
||||
|
||||
$actions .= view('components.datatables.sort-button', [
|
||||
'id' => $row->id,
|
||||
'module' => 'brand',
|
||||
'direction' => 'last',
|
||||
'tooltip' => 'Terakhir',
|
||||
'color' => 'rose',
|
||||
'icon' => 'bars-arrow-down',
|
||||
])->render();
|
||||
}
|
||||
|
||||
$actions .= view('components.datatables.edit-modal', [
|
||||
'id' => $row->id,
|
||||
'method' => 'update',
|
||||
'modalTitle' => 'Ubah Kategori',
|
||||
])->render();
|
||||
|
||||
$actions .= view('components.datatables.delete', [
|
||||
'id' => $row->id,
|
||||
'deleteRoute' => route('studio.catalog.brand.delete', $row->id),
|
||||
])->render();
|
||||
|
||||
return $actions;
|
||||
})
|
||||
->html(),
|
||||
];
|
||||
}
|
||||
|
||||
public function builder(): Builder
|
||||
{
|
||||
return Brand::select('id', 'name', 'sort_order')->orderBy('sort_order');
|
||||
}
|
||||
}
|
||||
122
app/Livewire/Forms/BrandForm.php
Normal file
122
app/Livewire/Forms/BrandForm.php
Normal file
@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Forms;
|
||||
|
||||
use App\Models\Brand;
|
||||
use App\Traits\WithMediaHandler;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Livewire\Form;
|
||||
|
||||
class BrandForm extends Form
|
||||
{
|
||||
use WithMediaHandler;
|
||||
|
||||
public ?Brand $brand = null;
|
||||
|
||||
public string $name = '';
|
||||
|
||||
public array $image = [];
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:20', Rule::unique('categories', 'name')->whereNull('deleted_at')->ignore($this->brand)],
|
||||
'image' => ['nullable', 'array'],
|
||||
];
|
||||
}
|
||||
|
||||
public function validationAttributes(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'nama',
|
||||
'image' => 'gambar',
|
||||
];
|
||||
}
|
||||
|
||||
public function setBrand(Brand $brand)
|
||||
{
|
||||
$this->brand = $brand;
|
||||
|
||||
$this->name = $brand->name;
|
||||
|
||||
$this->image = $this->mapMediaCollection($brand->getMedia('image'));
|
||||
}
|
||||
|
||||
public function store()
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
DB::transaction(function () {
|
||||
$brand = Brand::create([
|
||||
'name' => $this->name,
|
||||
'sort_order' => Brand::count() + 1,
|
||||
]);
|
||||
|
||||
$this->uploadMedia($this->image, $brand, 'image');
|
||||
});
|
||||
}
|
||||
|
||||
public function update()
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
DB::transaction(function () {
|
||||
$this->brand->update([
|
||||
'name' => $this->name,
|
||||
]);
|
||||
|
||||
$this->syncMedia($this->image, $this->brand, 'image');
|
||||
$this->uploadMedia($this->image, $this->brand, 'image');
|
||||
});
|
||||
}
|
||||
|
||||
public function delete()
|
||||
{
|
||||
$this->brand->delete();
|
||||
|
||||
Brand::where('sort_order', '>', $this->brand->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 = Brand::where('sort_order', '<', $this->brand->sort_order)
|
||||
->orderByDesc('sort_order')
|
||||
->first();
|
||||
} else {
|
||||
$swap = Brand::where('sort_order', '>', $this->brand->sort_order)
|
||||
->orderBy('sort_order')
|
||||
->first();
|
||||
}
|
||||
|
||||
if ($swap) {
|
||||
$temp = $this->brand->sort_order;
|
||||
$this->brand->update(['sort_order' => $swap->sort_order]);
|
||||
$swap->update(['sort_order' => $temp]);
|
||||
}
|
||||
} elseif ($direction === 'first') {
|
||||
$this->brand->update(['sort_order' => Brand::min('sort_order') - 1]);
|
||||
$this->normalizeSortOrder();
|
||||
} elseif ($direction === 'last') {
|
||||
$this->brand->update(['sort_order' => Brand::max('sort_order') + 1]);
|
||||
$this->normalizeSortOrder();
|
||||
}
|
||||
}
|
||||
|
||||
protected function normalizeSortOrder()
|
||||
{
|
||||
$categories = Brand::orderBy('sort_order')->get();
|
||||
foreach ($categories as $index => $brand) {
|
||||
$brand->update(['sort_order' => $index + 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
105
app/Livewire/Studio/Catalog/Brand.php
Normal file
105
app/Livewire/Studio/Catalog/Brand.php
Normal file
@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Studio\Catalog;
|
||||
|
||||
use App\Livewire\Forms\BrandForm;
|
||||
use App\Models\Brand as BrandModel;
|
||||
use App\Traits\WithCloseModal;
|
||||
use App\Traits\WithConfirmation;
|
||||
use App\Traits\WithUpdatedData;
|
||||
use Flux\Flux;
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Attributes\Title;
|
||||
use Livewire\Component;
|
||||
|
||||
#[Title('Merek')]
|
||||
class Brand extends Component
|
||||
{
|
||||
use WithCloseModal, WithConfirmation, WithUpdatedData;
|
||||
|
||||
public BrandForm $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->setBrand(BrandModel::findOrFail($id));
|
||||
}
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
$this->form->store();
|
||||
|
||||
$this->dispatch('refreshDatatable');
|
||||
|
||||
Flux::toast(
|
||||
heading: 'Berhasil',
|
||||
text: 'Merek berhasil ditambahkan.',
|
||||
variant: 'success',
|
||||
duration: 3000
|
||||
);
|
||||
|
||||
Flux::modals()->close();
|
||||
}
|
||||
|
||||
public function update()
|
||||
{
|
||||
$this->form->update();
|
||||
|
||||
$this->dispatch('refreshDatatable');
|
||||
|
||||
Flux::toast(
|
||||
heading: 'Berhasil',
|
||||
text: 'Merek berhasil diperbarui.',
|
||||
variant: 'success',
|
||||
duration: 3000
|
||||
);
|
||||
|
||||
Flux::modals()->close();
|
||||
}
|
||||
|
||||
public function delete(BrandModel $brand)
|
||||
{
|
||||
$this->form->brand = $brand;
|
||||
|
||||
$this->form->delete();
|
||||
|
||||
$this->dispatch('refreshDatatable');
|
||||
|
||||
Flux::toast(
|
||||
heading: 'Berhasil',
|
||||
text: 'Merek berhasil dihapus.',
|
||||
variant: 'success',
|
||||
);
|
||||
|
||||
Flux::modals()->close();
|
||||
}
|
||||
|
||||
#[On('fn:sortOrder')]
|
||||
public function sortOrder(BrandModel $brand, string $direction)
|
||||
{
|
||||
$this->form->brand = $brand;
|
||||
|
||||
$this->form->sortOrder($direction);
|
||||
|
||||
$this->dispatch('refreshDatatable');
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.studio.catalog.brands', [
|
||||
'pageTitle' => 'Merek',
|
||||
]);
|
||||
}
|
||||
}
|
||||
32
app/Models/Brand.php
Normal file
32
app/Models/Brand.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Spatie\MediaLibrary\HasMedia;
|
||||
use Spatie\MediaLibrary\InteractsWithMedia;
|
||||
use Spatie\Sluggable\HasSlug;
|
||||
use Spatie\Sluggable\SlugOptions;
|
||||
|
||||
class Brand extends Model implements HasMedia
|
||||
{
|
||||
use HasFactory, HasSlug, InteractsWithMedia, SoftDeletes;
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'sort_order' => 'int',
|
||||
];
|
||||
}
|
||||
|
||||
public function getSlugOptions(): SlugOptions
|
||||
{
|
||||
return SlugOptions::create()
|
||||
->generateSlugsFrom('name')
|
||||
->saveSlugsTo('slug');
|
||||
}
|
||||
}
|
||||
@ -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('brands', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name', 20);
|
||||
$table->string('slug', 30);
|
||||
$table->unsignedInteger('sort_order');
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('brands');
|
||||
}
|
||||
};
|
||||
@ -40,6 +40,10 @@ class="bg-zinc-50 dark:bg-zinc-900 border-r rtl:border-r-0 rtl:border-l border-z
|
||||
<flux:navlist.item icon="list-bullet" href="{{ route('studio.catalog.category.index') }}"
|
||||
:current="request()->routeIs('studio.catalog.category.*')" wire:navigate.hover>Kategori
|
||||
</flux:navlist.item>
|
||||
|
||||
<flux:navlist.item icon="tag" href="{{ route('studio.catalog.brand.index') }}"
|
||||
:current="request()->routeIs('studio.catalog.brand.*')" wire:navigate.hover>Merek
|
||||
</flux:navlist.item>
|
||||
</div>
|
||||
</div>
|
||||
</flux:navlist>
|
||||
|
||||
57
resources/views/livewire/studio/catalog/brands.blade.php
Normal file
57
resources/views/livewire/studio/catalog/brands.blade.php
Normal file
@ -0,0 +1,57 @@
|
||||
<flux:main>
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<flux:heading size="xl">{{ $pageTitle }}</flux:heading>
|
||||
</div>
|
||||
<div>
|
||||
<flux:modal.trigger name="form-modal">
|
||||
<flux:button variant="primary" class="text-sm"
|
||||
wire:click="$dispatch('modal:open', {method: 'create', 'modalTitle': 'Tambah Merek'})">Tambah
|
||||
</flux:button>
|
||||
</flux:modal.trigger>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<livewire:datatable.brands-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:input label="Nama" placeholder="Masukkan nama merek" wire:model.live.debounce.500ms="form.name"
|
||||
autofocus autocomplete="off" />
|
||||
|
||||
<div class="space-y-3">
|
||||
<h3 class="text-sm font-medium">Gambar Utama</h3>
|
||||
<div class="dropzone-wrapper">
|
||||
<livewire:dropzone wire:model="form.image" :rules="['image', 'mimes:png,jpeg', 'max:10420']" :max-files="1" :key="'image'"
|
||||
:files="$form->image ?? []" />
|
||||
@error('form.image')
|
||||
<div role="alert" aria-live="polite" aria-atomic="true"
|
||||
class="mt-3 text-sm font-medium text-red-500 dark:text-red-400">
|
||||
<svg class="shrink-0 [:where(&)]:size-5 inline" data-flux-icon=""
|
||||
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"
|
||||
aria-hidden="true" data-slot="icon">
|
||||
<path fill-rule="evenodd"
|
||||
d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495ZM10 5a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 10 5Zm0 9a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"
|
||||
clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
{{ $message }}
|
||||
</div>
|
||||
@enderror
|
||||
</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>
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
use App\Livewire\Auth\Login;
|
||||
use App\Livewire\Auth\Logout;
|
||||
use App\Livewire\Studio\Catalog\Brand as BrandComponent;
|
||||
use App\Livewire\Studio\Catalog\Category as CategoryComponent;
|
||||
use App\Livewire\Studio\Dashboard\Overview;
|
||||
use App\Livewire\Studio\Loyalty\Membership as MembershipComponent;
|
||||
@ -76,4 +77,11 @@
|
||||
Route::get('categories', CategoryComponent::class)->name('index');
|
||||
Route::delete('categories/{category}/delete', CategoryComponent::class)->name('delete');
|
||||
});
|
||||
|
||||
Route::prefix('catalog')
|
||||
->as('studio.catalog.brand.')
|
||||
->group(function () {
|
||||
Route::get('brands', BrandComponent::class)->name('index');
|
||||
Route::delete('brands/{brand}/delete', BrandComponent::class)->name('delete');
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user