diff --git a/app/Livewire/Datatable/BrandsTable.php b/app/Livewire/Datatable/BrandsTable.php new file mode 100644 index 0000000..dd7966e --- /dev/null +++ b/app/Livewire/Datatable/BrandsTable.php @@ -0,0 +1,89 @@ +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'); + } +} diff --git a/app/Livewire/Forms/BrandForm.php b/app/Livewire/Forms/BrandForm.php new file mode 100644 index 0000000..4d24aab --- /dev/null +++ b/app/Livewire/Forms/BrandForm.php @@ -0,0 +1,122 @@ + ['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]); + } + } +} diff --git a/app/Livewire/Studio/Catalog/Brand.php b/app/Livewire/Studio/Catalog/Brand.php new file mode 100644 index 0000000..b9308dd --- /dev/null +++ b/app/Livewire/Studio/Catalog/Brand.php @@ -0,0 +1,105 @@ +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', + ]); + } +} diff --git a/app/Models/Brand.php b/app/Models/Brand.php new file mode 100644 index 0000000..6a05241 --- /dev/null +++ b/app/Models/Brand.php @@ -0,0 +1,32 @@ + 'int', + ]; + } + + public function getSlugOptions(): SlugOptions + { + return SlugOptions::create() + ->generateSlugsFrom('name') + ->saveSlugsTo('slug'); + } +} diff --git a/database/migrations/2025_09_29_104530_create_brands_table.php b/database/migrations/2025_09_29_104530_create_brands_table.php new file mode 100644 index 0000000..9bc52ef --- /dev/null +++ b/database/migrations/2025_09_29_104530_create_brands_table.php @@ -0,0 +1,31 @@ +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'); + } +}; diff --git a/resources/views/components/partials/studio/_sidebar.blade.php b/resources/views/components/partials/studio/_sidebar.blade.php index 0725177..34bdd77 100644 --- a/resources/views/components/partials/studio/_sidebar.blade.php +++ b/resources/views/components/partials/studio/_sidebar.blade.php @@ -40,6 +40,10 @@ class="bg-zinc-50 dark:bg-zinc-900 border-r rtl:border-r-0 rtl:border-l border-z Kategori + + Merek + diff --git a/resources/views/livewire/studio/catalog/brands.blade.php b/resources/views/livewire/studio/catalog/brands.blade.php new file mode 100644 index 0000000..dc1e66a --- /dev/null +++ b/resources/views/livewire/studio/catalog/brands.blade.php @@ -0,0 +1,57 @@ + +
+
+ {{ $pageTitle }} +
+
+ + Tambah + + +
+
+ +
+ +
+ + @include('components.confirmation.delete') + +
+ {{ $modalTitle }} + + + +
+

Gambar Utama

+
+ + @error('form.image') + + @enderror +
+
+ +
+ + + Simpan + +
+
+
+ +
diff --git a/routes/web.php b/routes/web.php index ba2ae42..89cf3a0 100644 --- a/routes/web.php +++ b/routes/web.php @@ -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'); + }); });