From a0fd914af51e334b8e519cb44b15ccc9339298f5 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Wed, 1 Oct 2025 20:41:19 +0700 Subject: [PATCH] feat(perfume): crud parfum -menambahkan skema model dan migrsai -menyesuaikan relasi -membuat enum Concentration -mmebuat trait baru untuk handle category --- app/Enums/Concentration.php | 36 ++++ app/Livewire/Datatable/PerfumesTable.php | 95 ++++++++++ app/Livewire/Forms/PerfumeForm.php | 161 +++++++++++++++++ .../Studio/Catalog/Perfume/Create.php | 60 +++++++ app/Livewire/Studio/Catalog/Perfume/Edit.php | 63 +++++++ app/Livewire/Studio/Catalog/Perfume/Index.php | 37 ++++ app/Models/Brand.php | 6 + app/Models/Category.php | 6 + app/Models/CategoryPerfume.php | 12 ++ app/Models/Outlet.php | 5 + app/Models/OutletPerfume.php | 12 ++ app/Models/Perfume.php | 52 ++++++ app/Traits/WithCategorySelector.php | 16 ++ ...025_10_01_112147_create_perfumes_table.php | 40 +++++ ...1_112210_create_category_perfume_table.php | 29 ++++ ..._01_123039_create_outlet_perfume_table.php | 30 ++++ .../partials/studio/_sidebar.blade.php | 4 + .../studio/catalog/perfume/form.blade.php | 162 ++++++++++++++++++ .../studio/catalog/perfume/index.blade.php | 19 ++ routes/web.php | 12 ++ 20 files changed, 857 insertions(+) create mode 100644 app/Enums/Concentration.php create mode 100644 app/Livewire/Datatable/PerfumesTable.php create mode 100644 app/Livewire/Forms/PerfumeForm.php create mode 100644 app/Livewire/Studio/Catalog/Perfume/Create.php create mode 100644 app/Livewire/Studio/Catalog/Perfume/Edit.php create mode 100644 app/Livewire/Studio/Catalog/Perfume/Index.php create mode 100644 app/Models/CategoryPerfume.php create mode 100644 app/Models/OutletPerfume.php create mode 100644 app/Models/Perfume.php create mode 100644 app/Traits/WithCategorySelector.php create mode 100644 database/migrations/2025_10_01_112147_create_perfumes_table.php create mode 100644 database/migrations/2025_10_01_112210_create_category_perfume_table.php create mode 100644 database/migrations/2025_10_01_123039_create_outlet_perfume_table.php create mode 100644 resources/views/livewire/studio/catalog/perfume/form.blade.php create mode 100644 resources/views/livewire/studio/catalog/perfume/index.blade.php diff --git a/app/Enums/Concentration.php b/app/Enums/Concentration.php new file mode 100644 index 0000000..a030299 --- /dev/null +++ b/app/Enums/Concentration.php @@ -0,0 +1,36 @@ + 'Extrait de parfum', + self::EAU_DE_PERFUME => 'Eau de parfum', + self::EAU_DE_COLOGNE => 'Eau de Cologne', + self::EAU_DE_TOILETTE => 'Eau de toilette', + }; + } + + public function color() + { + return match ($this) { + self::EXTRAIT_DE_PERFUME => 'emerald', + self::EAU_DE_PERFUME => 'rose', + self::EAU_DE_COLOGNE => 'cyan', + self::EAU_DE_TOILETTE => 'indigo', + }; + } +} diff --git a/app/Livewire/Datatable/PerfumesTable.php b/app/Livewire/Datatable/PerfumesTable.php new file mode 100644 index 0000000..7f42f98 --- /dev/null +++ b/app/Livewire/Datatable/PerfumesTable.php @@ -0,0 +1,95 @@ +label(function ($row) { + return << +
{$row->name}
+ {$row->sku} + + HTML; + }) + ->searchable(function ($query, $searchTerm) { + $query->where('name', 'like', "%{$searchTerm}%") + ->orWhere('sku', 'like', "%{$searchTerm}%"); + }) + ->html(), + + ArrayColumn::make('Kategori') + ->data(fn ($value, $row) => $row->categories->pluck('name')->toArray()) + ->outputFormat(fn ($index, $value) => Blade::render(''.$value.'')) + ->flexRow(['class' => 'gap-2 flex-wrap']), + + Column::make('Merek', 'brand.name')->searchable()->sortable(), + + Column::make('Concentration', 'concentration') + ->label(fn ($row, $column) => Blade::render(''.$row->concentration->label().'')) + ->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(), + + Column::make('Base Notes', 'base_notes')->searchable()->sortable(), + + Column::make('Middle Notes', 'middle_notes')->searchable()->sortable(), + + Column::make('Top Notes', 'top_notes')->searchable()->sortable(), + + 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.perfume.edit', $row->id), + ])->render(); + + $actions .= view('components.datatables.delete', [ + 'id' => $row->id, + 'deleteRoute' => route('studio.catalog.perfume.delete', $row->id), + ])->render(); + + return $actions; + }) + ->html(), + ]; + } + + public function builder(): Builder + { + return Perfume::select('perfumes.id', 'sku', 'perfumes.name', 'concentration', 'cost_price', 'sale_price', 'base_notes', 'middle_notes', 'top_notes')->with(['categories', 'brand', 'outlets']); + } +} diff --git a/app/Livewire/Forms/PerfumeForm.php b/app/Livewire/Forms/PerfumeForm.php new file mode 100644 index 0000000..c1a6e65 --- /dev/null +++ b/app/Livewire/Forms/PerfumeForm.php @@ -0,0 +1,161 @@ + ['required', 'string', 'max:50'], + 'sku' => [ + 'required', + 'string', + 'max:20', + Rule::unique('perfumes', 'sku')->ignore($this->perfume), + ], + 'brand' => ['nullable', Rule::exists('brands', 'id')], + 'cost_price' => ['required', 'numeric', new UnsignedInteger], + 'sale_price' => ['required', 'numeric', new UnsignedInteger], + 'base_notes' => ['nullable', 'string', 'max:255'], + 'middle_notes' => ['nullable', 'string', 'max:255'], + 'top_notes' => ['nullable', 'string', 'max:255'], + 'description' => ['nullable', 'string'], + 'concentration' => ['required', Rule::in(Concentration::values())], + 'category_ids' => ['required', 'array', 'min:1'], + 'category_ids.*' => Rule::exists('categories', 'id'), + 'outlet_ids' => ['required', 'array', 'min:1'], + 'outlet_ids.*' => Rule::exists('outlets', 'id'), + 'image' => ['nullable', 'array', 'max:1'], + ]; + } + + public function validationAttributes(): array + { + return [ + 'name' => 'nama', + 'brand' => 'merek', + 'cost_price' => 'harga beli', + 'sale_price' => 'harga jual', + 'description' => 'deskripsi', + 'category_ids' => 'kategori', + 'outlet_ids' => 'outlet', + 'image' => 'gambar', + ]; + } + + public function setPerfume(Perfume $perfume) + { + $perfume->load(['brand', 'outlets', 'categories']); + + $this->perfume = $perfume; + + $this->name = $perfume->name; + $this->sku = $perfume->sku; + $this->brand = $perfume->brand_id; + $this->cost_price = $perfume->cost_price; + $this->sale_price = $perfume->sale_price; + $this->base_notes = $perfume->base_notes; + $this->middle_notes = $perfume->middle_notes; + $this->top_notes = $perfume->top_notes; + $this->description = $perfume->description; + $this->concentration = $perfume->concentration->value; + $this->category_ids = $perfume->categories->pluck('id')->toArray(); + $this->outlet_ids = $this->perfume->outlets->pluck('id')->toArray(); + $this->image = $this->mapMediaCollection($perfume->getMedia('image')); + } + + public function store() + { + $this->validate(); + + $data = $this->prepareSavedData(); + + DB::transaction(function () use ($data) { + $perfume = Perfume::create($data); + + $perfume->categories()->attach($this->category_ids); + + $perfume->outlets()->attach($this->outlet_ids); + + if ($this->image) { + $this->image = $this->mapMediaCollection($perfume->getMedia('image')); + } + }); + } + + public function update() + { + $this->validate(); + + $data = $this->prepareSavedData(); + + DB::transaction(function () use ($data) { + $this->perfume->update($data); + + $this->perfume->categories()->sync($this->category_ids); + + $this->perfume->outlets()->sync($this->outlet_ids); + + $this->syncMedia($data['image'] ?? [], $this->perfume, 'image'); + $this->uploadMedia($data['image'] ?? [], $this->perfume, 'image'); + }); + } + + private function prepareSavedData() + { + return [ + 'name' => $this->name, + 'sku' => $this->sku, + 'cost_price' => $this->cost_price, + 'sale_price' => $this->sale_price, + 'base_notes' => $this->base_notes, + 'middle_notes' => $this->middle_notes, + 'top_notes' => $this->top_notes, + 'description' => $this->description, + 'concentration' => $this->concentration, + 'brand_id' => $this->brand, + 'category_ids' => $this->category_ids, + 'outlet_ids' => $this->outlet_ids, + 'image' => $this->image, + ]; + } +} diff --git a/app/Livewire/Studio/Catalog/Perfume/Create.php b/app/Livewire/Studio/Catalog/Perfume/Create.php new file mode 100644 index 0000000..6ffa25f --- /dev/null +++ b/app/Livewire/Studio/Catalog/Perfume/Create.php @@ -0,0 +1,60 @@ +outlets = Outlet::pluck('name', 'id')->toArray(); + + $this->brands = Brand::pluck('name', 'id')->toArray(); + + $this->categories = Category::pluck('name', 'id')->toArray(); + } + + public function save() + { + $this->form->store(); + + $this->dispatch('refreshDatatable'); + + Flux::toast( + heading: 'Berhasil', + text: 'Parfum berhasil ditambahkan.', + variant: 'success', + duration: 3000 + ); + + $this->redirectRoute('studio.catalog.perfume.index', navigate: true); + } + + public function render() + { + return view('livewire.studio.catalog.perfume.form', [ + 'pageTitle' => 'Tambah Parfum', + ]); + } +} diff --git a/app/Livewire/Studio/Catalog/Perfume/Edit.php b/app/Livewire/Studio/Catalog/Perfume/Edit.php new file mode 100644 index 0000000..462a27a --- /dev/null +++ b/app/Livewire/Studio/Catalog/Perfume/Edit.php @@ -0,0 +1,63 @@ +form->setPerfume($perfume); + + $this->outlets = Outlet::pluck('name', 'id')->toArray(); + + $this->brands = Brand::pluck('name', 'id')->toArray(); + + $this->categories = Category::pluck('name', 'id')->toArray(); + } + + public function save() + { + $this->form->update(); + + $this->dispatch('refreshDatatable'); + + Flux::toast( + heading: 'Berhasil', + text: 'Parfum berhasil diperbarui.', + variant: 'success', + duration: 3000 + ); + + $this->redirectRoute('studio.catalog.perfume.index', navigate: true); + } + + public function render() + { + return view('livewire.studio.catalog.perfume.form', [ + 'pageTitle' => 'Ubah Parfum', + ]); + } +} diff --git a/app/Livewire/Studio/Catalog/Perfume/Index.php b/app/Livewire/Studio/Catalog/Perfume/Index.php new file mode 100644 index 0000000..cab80bc --- /dev/null +++ b/app/Livewire/Studio/Catalog/Perfume/Index.php @@ -0,0 +1,37 @@ +delete(); + + $this->dispatch('refreshDatatable'); + + Flux::toast( + heading: 'Berhasil', + text: 'Parfum berhasil dihapus.', + variant: 'success', + ); + + Flux::modals()->close(); + } + + public function render() + { + return view('livewire.studio.catalog.perfume.index', [ + 'pageTitle' => 'Parfum', + ]); + } +} diff --git a/app/Models/Brand.php b/app/Models/Brand.php index 6a05241..cdce4e9 100644 --- a/app/Models/Brand.php +++ b/app/Models/Brand.php @@ -4,6 +4,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; use Spatie\MediaLibrary\HasMedia; use Spatie\MediaLibrary\InteractsWithMedia; @@ -29,4 +30,9 @@ public function getSlugOptions(): SlugOptions ->generateSlugsFrom('name') ->saveSlugsTo('slug'); } + + public function perfumes(): HasMany + { + return $this->hasMany(Perfume::class); + } } diff --git a/app/Models/Category.php b/app/Models/Category.php index 81cf951..e193ccd 100644 --- a/app/Models/Category.php +++ b/app/Models/Category.php @@ -4,6 +4,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\SoftDeletes; use Spatie\Sluggable\HasSlug; use Spatie\Sluggable\SlugOptions; @@ -27,4 +28,9 @@ public function getSlugOptions(): SlugOptions ->generateSlugsFrom('name') ->saveSlugsTo('slug'); } + + public function perfumes(): BelongsToMany + { + return $this->belongsToMany(Perfume::class); + } } diff --git a/app/Models/CategoryPerfume.php b/app/Models/CategoryPerfume.php new file mode 100644 index 0000000..fc8aa74 --- /dev/null +++ b/app/Models/CategoryPerfume.php @@ -0,0 +1,12 @@ +belongsToMany(Voucher::class); } + + public function perfumes(): BelongsToMany + { + return $this->belongsToMany(Perfume::class); + } } diff --git a/app/Models/OutletPerfume.php b/app/Models/OutletPerfume.php new file mode 100644 index 0000000..d54106a --- /dev/null +++ b/app/Models/OutletPerfume.php @@ -0,0 +1,12 @@ + Concentration::class, + 'cost_price' => 'int', + 'sale_price' => 'int', + ]; + } + + public function getSlugOptions(): SlugOptions + { + return SlugOptions::create() + ->generateSlugsFrom('name') + ->saveSlugsTo('slug'); + } + + public function brand(): BelongsTo + { + return $this->belongsTo(Brand::class); + } + + public function categories(): BelongsToMany + { + return $this->belongsToMany(Category::class); + } + + public function outlets(): BelongsToMany + { + return $this->belongsToMany(Outlet::class); + } +} diff --git a/app/Traits/WithCategorySelector.php b/app/Traits/WithCategorySelector.php new file mode 100644 index 0000000..b4aad4e --- /dev/null +++ b/app/Traits/WithCategorySelector.php @@ -0,0 +1,16 @@ +form->category_ids = array_keys($this->categories); + } + + public function deselectAllCategories() + { + $this->form->category_ids = []; + } +} diff --git a/database/migrations/2025_10_01_112147_create_perfumes_table.php b/database/migrations/2025_10_01_112147_create_perfumes_table.php new file mode 100644 index 0000000..b42bbe4 --- /dev/null +++ b/database/migrations/2025_10_01_112147_create_perfumes_table.php @@ -0,0 +1,40 @@ +id(); + $table->foreignId('brand_id')->nullable()->constrained()->cascadeOnDelete(); + $table->string('name', 50); + $table->string('slug', 70)->unique(); + $table->string('sku', 20)->unique(); + $table->enum('concentration', [Concentration::values()])->default(Concentration::EXTRAIT_DE_PERFUME)->comment(Concentration::comment()); + $table->unsignedInteger('cost_price')->default(0); + $table->unsignedInteger('sale_price')->default(0); + $table->string('base_notes')->nullable(); + $table->string('middle_notes')->nullable(); + $table->string('top_notes')->nullable(); + $table->text('description')->nullable(); + $table->timestamps(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('perfumes'); + } +}; diff --git a/database/migrations/2025_10_01_112210_create_category_perfume_table.php b/database/migrations/2025_10_01_112210_create_category_perfume_table.php new file mode 100644 index 0000000..3df3208 --- /dev/null +++ b/database/migrations/2025_10_01_112210_create_category_perfume_table.php @@ -0,0 +1,29 @@ +id(); + $table->foreignId('category_id')->constrained()->cascadeOnDelete(); + $table->foreignId('perfume_id')->constrained()->cascadeOnDelete(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('category_perfume'); + } +}; diff --git a/database/migrations/2025_10_01_123039_create_outlet_perfume_table.php b/database/migrations/2025_10_01_123039_create_outlet_perfume_table.php new file mode 100644 index 0000000..b0eb48f --- /dev/null +++ b/database/migrations/2025_10_01_123039_create_outlet_perfume_table.php @@ -0,0 +1,30 @@ +id(); + $table->foreignId('outlet_id')->constrained()->cascadeOnDelete(); + $table->foreignId('perfume_id')->constrained()->cascadeOnDelete(); + $table->unsignedInteger('stock')->default(0); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('outlet_perfume'); + } +}; diff --git a/resources/views/components/partials/studio/_sidebar.blade.php b/resources/views/components/partials/studio/_sidebar.blade.php index 0073510..3fe89bc 100644 --- a/resources/views/components/partials/studio/_sidebar.blade.php +++ b/resources/views/components/partials/studio/_sidebar.blade.php @@ -48,6 +48,10 @@ class="bg-zinc-50 dark:bg-zinc-900 border-r rtl:border-r-0 rtl:border-l border-z Merek + + Parfum + diff --git a/resources/views/livewire/studio/catalog/perfume/form.blade.php b/resources/views/livewire/studio/catalog/perfume/form.blade.php new file mode 100644 index 0000000..642f381 --- /dev/null +++ b/resources/views/livewire/studio/catalog/perfume/form.blade.php @@ -0,0 +1,162 @@ + +
+
+ {{ $pageTitle }} +
+
+ + Kembali + +
+
+ +
+
+
+
+
+
+ +
+ + + + +
+
+ + @foreach ($brands as $key => $value) + + {{ $value }} + + @endforeach + + + + Harga Beli + + Rp + + + + + + + Harga Jual + + Rp + + + + +
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+
+
+
+ +
+ + + @foreach (\App\Enums\Concentration::cases() as $item) + + {{ $item->label() }} + + @endforeach + + + + + + Pilih Semua + + Hapus Semua + + @foreach ($categories as $key => $value) + {{ $value }} + + @endforeach + + + + + + 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/perfume/index.blade.php b/resources/views/livewire/studio/catalog/perfume/index.blade.php new file mode 100644 index 0000000..cd438ae --- /dev/null +++ b/resources/views/livewire/studio/catalog/perfume/index.blade.php @@ -0,0 +1,19 @@ + +
+
+ {{ $pageTitle }} +
+
+ + Tambah + +
+
+ +
+ +
+ + @include('components.confirmation.delete') +
diff --git a/routes/web.php b/routes/web.php index 8c3418b..3bddc07 100644 --- a/routes/web.php +++ b/routes/web.php @@ -5,6 +5,9 @@ use App\Livewire\Auth\Register; 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; +use App\Livewire\Studio\Catalog\Perfume\Edit as PerfumeEdit; +use App\Livewire\Studio\Catalog\Perfume\Index as PerfumeIndex; use App\Livewire\Studio\Dashboard\Overview; use App\Livewire\Studio\Loyalty\Customer as CustomerComponent; use App\Livewire\Studio\Loyalty\Tier as TierComponent; @@ -115,4 +118,13 @@ Route::get('brands', BrandComponent::class)->name('index'); Route::delete('brands/{brand}/delete', BrandComponent::class)->name('delete'); }); + + Route::prefix('catalog') + ->as('studio.catalog.perfume.') + ->group(function () { + Route::get('perfumes', PerfumeIndex::class)->name('index'); + Route::get('perfumes/create', PerfumeCreate::class)->name('create'); + Route::get('perfumes/{perfume}/edit', PerfumeEdit::class)->name('edit'); + Route::get('perfumes/{perfume}/delete', PerfumeCreate::class)->name('delete'); + }); });