diff --git a/app/Livewire/Datatable/BottlesTable.php b/app/Livewire/Datatable/BottlesTable.php
new file mode 100644
index 0000000..861459a
--- /dev/null
+++ b/app/Livewire/Datatable/BottlesTable.php
@@ -0,0 +1,86 @@
+label(function ($row) {
+ return <<
+
{$row->name}
+ {$row->size} ml
+
+ HTML;
+ })
+ ->searchable(function ($query, $searchTerm) {
+ $query->where('name', 'like', "%{$searchTerm}%")
+ ->orWhere('size', 'like', "%{$searchTerm}%");
+ })
+ ->html(),
+
+ Column::make('Harga Beli', 'cost_price')
+ ->label(fn ($row, $column) => currency($row->cost_price, 'Rp'))
+ ->searchable()
+ ->sortable(),
+
+ Column::make('Harga Jual', 'sale_price')
+ ->label(fn ($row, $column) => currency($row->sale_price, 'Rp'))
+ ->searchable()
+ ->sortable(),
+
+ ImageColumn::make('Gambar')
+ ->location(fn ($row) => optional($row->getMedia('image')->first())->getUrl() ?? asset('assets/images/logo.png'))
+ ->attributes(fn ($row) => [
+ 'style' => 'width: 50px; height: 50px;',
+ 'alt' => $row->name,
+ ]),
+
+ ArrayColumn::make('Outlet')
+ ->data(fn ($value, $row) => $row->outlets->pluck('name')->toArray())
+ ->outputFormat(fn ($index, $value) => Blade::render(''.$value.''))
+ ->flexRow(['class' => 'gap-2 flex-wrap']),
+
+ Column::make('Aksi')
+ ->label(function ($row) {
+ $actions = '';
+
+ $actions .= view('components.datatables.edit', [
+ 'id' => $row->id,
+ 'editRoute' => route('studio.catalog.bottle.edit', $row->id),
+ ])->render();
+
+ $actions .= view('components.datatables.delete', [
+ 'id' => $row->id,
+ 'deleteRoute' => route('studio.catalog.bottle.delete', $row->id),
+ ])->render();
+
+ return $actions;
+ })
+ ->html(),
+ ];
+ }
+
+ public function builder(): Builder
+ {
+ return Bottle::select('id', 'name', 'size', 'cost_price', 'sale_price')->with('outlets');
+ }
+}
diff --git a/app/Livewire/Forms/BottleForm.php b/app/Livewire/Forms/BottleForm.php
new file mode 100644
index 0000000..1b523cd
--- /dev/null
+++ b/app/Livewire/Forms/BottleForm.php
@@ -0,0 +1,100 @@
+ ['required', 'string', 'max:50'],
+ 'size' => ['required', 'numeric', new UnsignedInteger],
+ 'cost_price' => ['required', 'numeric', new UnsignedInteger],
+ 'sale_price' => ['required', 'numeric', new UnsignedInteger],
+ 'description' => ['nullable', 'string'],
+ 'image' => ['nullable', 'array'],
+ ];
+ }
+
+ public function validationAttributes(): array
+ {
+ return [
+ 'name' => 'nama',
+ 'size' => 'ukuran',
+ 'cost_price' => 'harga beli',
+ 'sale_price' => 'harga jual',
+ 'description' => 'deskripsi',
+ 'image' => 'gambar',
+ ];
+ }
+
+ public function setBottle(Bottle $bottle)
+ {
+ $this->bottle = $bottle;
+
+ $this->name = $bottle->name;
+ $this->size = $bottle->size;
+ $this->cost_price = $bottle->cost_price;
+ $this->sale_price = $bottle->sale_price;
+ $this->description = $bottle->description;
+
+ $this->image = $this->mapMediaCollection($bottle->getMedia('image'));
+ }
+
+ public function store()
+ {
+ $this->validate();
+
+ DB::transaction(function () {
+ $bottle = Bottle::create([
+ 'name' => $this->name,
+ 'size' => $this->size,
+ 'cost_price' => $this->cost_price,
+ 'sale_price' => $this->sale_price,
+ 'description' => $this->description,
+ ]);
+
+ $this->uploadMedia($this->image, $bottle, 'image');
+ });
+ }
+
+ public function update()
+ {
+ $this->validate();
+
+ DB::transaction(function () {
+ $this->bottle->update([
+ 'name' => $this->name,
+ 'size' => $this->size,
+ 'cost_price' => $this->cost_price,
+ 'sale_price' => $this->sale_price,
+ 'description' => $this->description,
+ ]);
+
+ $this->syncMedia($this->image, $this->bottle, 'image');
+ $this->uploadMedia($this->image, $this->bottle, 'image');
+ });
+ }
+}
diff --git a/app/Livewire/Studio/Catalog/Bottle/Create.php b/app/Livewire/Studio/Catalog/Bottle/Create.php
new file mode 100644
index 0000000..62e1803
--- /dev/null
+++ b/app/Livewire/Studio/Catalog/Bottle/Create.php
@@ -0,0 +1,49 @@
+outlets = Outlet::pluck('name', 'id')->toArray();
+ }
+
+ public function save()
+ {
+ $this->form->store();
+
+ $this->dispatch('refreshDatatable');
+
+ Flux::toast(
+ heading: 'Berhasil',
+ text: 'Botol berhasil ditambahkan.',
+ variant: 'success',
+ duration: 3000
+ );
+
+ $this->redirectRoute('studio.catalog.bottle.index', navigate: true);
+ }
+
+ public function render()
+ {
+ return view('livewire.studio.catalog.bottle.form', [
+ 'pageTitle' => 'Tambah Botol',
+ ]);
+ }
+}
diff --git a/app/Livewire/Studio/Catalog/Bottle/Edit.php b/app/Livewire/Studio/Catalog/Bottle/Edit.php
new file mode 100644
index 0000000..2e34aaa
--- /dev/null
+++ b/app/Livewire/Studio/Catalog/Bottle/Edit.php
@@ -0,0 +1,52 @@
+form->setBottle($bottle);
+
+ $this->outlets = Outlet::pluck('name', 'id')->toArray();
+ }
+
+ public function save()
+ {
+ $this->form->update();
+
+ $this->dispatch('refreshDatatable');
+
+ Flux::toast(
+ heading: 'Berhasil',
+ text: 'Botol berhasil diperbarui.',
+ variant: 'success',
+ duration: 3000
+ );
+
+ $this->redirectRoute('studio.catalog.bottle.index', navigate: true);
+ }
+
+ public function render()
+ {
+ return view('livewire.studio.catalog.bottle.form', [
+ 'pageTitle' => 'Ubah Botol',
+ ]);
+ }
+}
diff --git a/app/Livewire/Studio/Catalog/Bottle/Index.php b/app/Livewire/Studio/Catalog/Bottle/Index.php
new file mode 100644
index 0000000..ec466ea
--- /dev/null
+++ b/app/Livewire/Studio/Catalog/Bottle/Index.php
@@ -0,0 +1,37 @@
+delete();
+
+ $this->dispatch('refreshDatatable');
+
+ Flux::toast(
+ heading: 'Berhasil',
+ text: 'Botol berhasil dihapus.',
+ variant: 'success',
+ );
+
+ Flux::modals()->close();
+ }
+
+ public function render()
+ {
+ return view('livewire.studio.catalog.bottle.index', [
+ 'pageTitle' => 'Botol',
+ ]);
+ }
+}
diff --git a/app/Models/Bottle.php b/app/Models/Bottle.php
new file mode 100644
index 0000000..bcea0e4
--- /dev/null
+++ b/app/Models/Bottle.php
@@ -0,0 +1,39 @@
+ 'int',
+ 'sale_price' => 'int',
+ ];
+ }
+
+ public function getSlugOptions(): SlugOptions
+ {
+ return SlugOptions::create()
+ ->generateSlugsFrom('name')
+ ->saveSlugsTo('slug');
+ }
+
+ public function outlets(): BelongsToMany
+ {
+ return $this->belongsToMany(Outlet::class);
+ }
+}
diff --git a/app/Models/BottleOutlet.php b/app/Models/BottleOutlet.php
new file mode 100644
index 0000000..2ab838a
--- /dev/null
+++ b/app/Models/BottleOutlet.php
@@ -0,0 +1,12 @@
+belongsToMany(Product::class);
}
+
+ public function bottles(): BelongsToMany
+ {
+ return $this->belongsToMany(Bottle::class);
+ }
}
diff --git a/database/migrations/2025_10_02_020223_create_bottles_table.php b/database/migrations/2025_10_02_020223_create_bottles_table.php
new file mode 100644
index 0000000..f5cdf1d
--- /dev/null
+++ b/database/migrations/2025_10_02_020223_create_bottles_table.php
@@ -0,0 +1,34 @@
+id();
+ $table->string('name', 50);
+ $table->string('slug', 70)->unique();
+ $table->unsignedInteger('size');
+ $table->unsignedInteger('cost_price')->default(0);
+ $table->unsignedInteger('sale_price')->default(0);
+ $table->text('description')->nullable();
+ $table->timestamps();
+ $table->softDeletes();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('bottles');
+ }
+};
diff --git a/database/migrations/2025_10_02_020233_create_bottle_outlet_table.php b/database/migrations/2025_10_02_020233_create_bottle_outlet_table.php
new file mode 100644
index 0000000..c11d17c
--- /dev/null
+++ b/database/migrations/2025_10_02_020233_create_bottle_outlet_table.php
@@ -0,0 +1,30 @@
+id();
+ $table->foreignId('bottle_id')->constrained()->cascadeOnDelete();
+ $table->foreignId('outlet_id')->constrained()->cascadeOnDelete();
+ $table->unsignedInteger('stock')->default(0);
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('bottle_outlet');
+ }
+};
diff --git a/resources/views/components/partials/studio/_sidebar.blade.php b/resources/views/components/partials/studio/_sidebar.blade.php
index 1a637c1..c23af4d 100644
--- a/resources/views/components/partials/studio/_sidebar.blade.php
+++ b/resources/views/components/partials/studio/_sidebar.blade.php
@@ -56,6 +56,10 @@ class="bg-zinc-50 dark:bg-zinc-900 border-r rtl:border-r-0 rtl:border-l border-z
Produk
+
+ Botol
+
diff --git a/resources/views/livewire/studio/catalog/bottle/form.blade.php b/resources/views/livewire/studio/catalog/bottle/form.blade.php
new file mode 100644
index 0000000..10ff950
--- /dev/null
+++ b/resources/views/livewire/studio/catalog/bottle/form.blade.php
@@ -0,0 +1,107 @@
+
+
+
+ {{ $pageTitle }}
+
+
+
+ Kembali
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Harga Beli
+
+ Rp
+
+
+
+
+
+
+ Harga Jual
+
+ Rp
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Pilih Semua
+
+ Hapus Semua
+
+ @foreach ($outlets as $key => $value)
+ {{ $value }}
+
+ @endforeach
+
+
+
+
+
+
Gambar
+
+
+ @error('form.image')
+
+ @enderror
+
+
+
+
+
+
+
+
+ Simpan
+
+
+
+
+
diff --git a/resources/views/livewire/studio/catalog/bottle/index.blade.php b/resources/views/livewire/studio/catalog/bottle/index.blade.php
new file mode 100644
index 0000000..3c23062
--- /dev/null
+++ b/resources/views/livewire/studio/catalog/bottle/index.blade.php
@@ -0,0 +1,19 @@
+
+
+
+ {{ $pageTitle }}
+
+
+
+ Tambah
+
+
+
+
+
+
+
+
+ @include('components.confirmation.delete')
+
diff --git a/routes/web.php b/routes/web.php
index 0ef5948..dfb3c25 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -3,6 +3,9 @@
use App\Livewire\Auth\Login;
use App\Livewire\Auth\Logout;
use App\Livewire\Auth\Register;
+use App\Livewire\Studio\Catalog\Bottle\Create as BottleCreate;
+use App\Livewire\Studio\Catalog\Bottle\Edit as BottleEdit;
+use App\Livewire\Studio\Catalog\Bottle\Index as BottleIndex;
use App\Livewire\Studio\Catalog\Brand as BrandComponent;
use App\Livewire\Studio\Catalog\Category as CategoryComponent;
use App\Livewire\Studio\Catalog\Perfume\Create as PerfumeCreate;
@@ -139,4 +142,13 @@
Route::get('products/{product}/edit', ProductEdit::class)->name('edit');
Route::get('products/{product}/delete', ProductCreate::class)->name('delete');
});
+
+ Route::prefix('catalog')
+ ->as('studio.catalog.bottle.')
+ ->group(function () {
+ Route::get('bottles', BottleIndex::class)->name('index');
+ Route::get('bottles/create', BottleCreate::class)->name('create');
+ Route::get('bottles/{bottle}/edit', BottleEdit::class)->name('edit');
+ Route::get('bottles/{bottle}/delete', BottleCreate::class)->name('delete');
+ });
});