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');
+ });
});