feat(perfume): crud parfum
-menambahkan skema model dan migrsai -menyesuaikan relasi -membuat enum Concentration -mmebuat trait baru untuk handle category
This commit is contained in:
parent
20f393d326
commit
a0fd914af5
36
app/Enums/Concentration.php
Normal file
36
app/Enums/Concentration.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use App\Traits\WithCommentEnum;
|
||||
use App\Traits\WithValueEnum;
|
||||
|
||||
enum Concentration: int
|
||||
{
|
||||
use WithCommentEnum, WithValueEnum;
|
||||
|
||||
case EXTRAIT_DE_PERFUME = 1;
|
||||
case EAU_DE_PERFUME = 2;
|
||||
case EAU_DE_COLOGNE = 3;
|
||||
case EAU_DE_TOILETTE = 4;
|
||||
|
||||
public function label()
|
||||
{
|
||||
return match ($this) {
|
||||
self::EXTRAIT_DE_PERFUME => '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',
|
||||
};
|
||||
}
|
||||
}
|
||||
95
app/Livewire/Datatable/PerfumesTable.php
Normal file
95
app/Livewire/Datatable/PerfumesTable.php
Normal file
@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Datatable;
|
||||
|
||||
use App\Models\Perfume;
|
||||
use App\Traits\Datatable\WithAppendColumn;
|
||||
use App\Traits\Datatable\WithConfiguration;
|
||||
use App\Traits\Datatable\WithPrependColumn;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Rappasoft\LaravelLivewireTables\DataTableComponent;
|
||||
use Rappasoft\LaravelLivewireTables\Views\Column;
|
||||
use Rappasoft\LaravelLivewireTables\Views\Columns\ArrayColumn;
|
||||
|
||||
class PerfumesTable extends DataTableComponent
|
||||
{
|
||||
use WithAppendColumn, WithConfiguration, WithPrependColumn;
|
||||
|
||||
protected $model = Perfume::class;
|
||||
|
||||
public function columns(): array
|
||||
{
|
||||
return [
|
||||
Column::make('Nama')
|
||||
->label(function ($row) {
|
||||
return <<<HTML
|
||||
<div>
|
||||
<div>{$row->name}</div>
|
||||
<span class="text-xs text-gray-400">{$row->sku}</span>
|
||||
</div>
|
||||
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('<span class="text-xs text-gray-400 border border-gray-400 rounded px-1">'.$value.'</span>'))
|
||||
->flexRow(['class' => 'gap-2 flex-wrap']),
|
||||
|
||||
Column::make('Merek', 'brand.name')->searchable()->sortable(),
|
||||
|
||||
Column::make('Concentration', 'concentration')
|
||||
->label(fn ($row, $column) => Blade::render('<flux:badge color="'.$row->concentration->color().'">'.$row->concentration->label().'</flux:badge>'))
|
||||
->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('<span class="text-xs text-gray-400 border border-gray-400 rounded px-1">'.$value.'</span>'))
|
||||
->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']);
|
||||
}
|
||||
}
|
||||
161
app/Livewire/Forms/PerfumeForm.php
Normal file
161
app/Livewire/Forms/PerfumeForm.php
Normal file
@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Forms;
|
||||
|
||||
use App\Enums\Concentration;
|
||||
use App\Models\Perfume;
|
||||
use App\Rules\UnsignedInteger;
|
||||
use App\Traits\WithMediaHandler;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Livewire\Form;
|
||||
|
||||
class PerfumeForm extends Form
|
||||
{
|
||||
use WithMediaHandler;
|
||||
|
||||
public ?Perfume $perfume = null;
|
||||
|
||||
public string $name = '';
|
||||
|
||||
public string $sku = '';
|
||||
|
||||
public ?string $brand = null;
|
||||
|
||||
public string $cost_price = '';
|
||||
|
||||
public string $sale_price = '';
|
||||
|
||||
public ?string $base_notes = null;
|
||||
|
||||
public ?string $middle_notes = null;
|
||||
|
||||
public ?string $top_notes = null;
|
||||
|
||||
public ?string $description = null;
|
||||
|
||||
public string $concentration = '';
|
||||
|
||||
public array $category_ids = [];
|
||||
|
||||
public ?array $image = null;
|
||||
|
||||
public array $outlet_ids = [];
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['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,
|
||||
];
|
||||
}
|
||||
}
|
||||
60
app/Livewire/Studio/Catalog/Perfume/Create.php
Normal file
60
app/Livewire/Studio/Catalog/Perfume/Create.php
Normal file
@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Studio\Catalog\Perfume;
|
||||
|
||||
use App\Livewire\Forms\PerfumeForm;
|
||||
use App\Models\Brand;
|
||||
use App\Models\Category;
|
||||
use App\Models\Outlet;
|
||||
use App\Traits\WithCategorySelector;
|
||||
use App\Traits\WithOutletSelector;
|
||||
use App\Traits\WithUpdatedData;
|
||||
use Flux\Flux;
|
||||
use Livewire\Attributes\Title;
|
||||
use Livewire\Component;
|
||||
|
||||
#[Title('Tambah Parfum')]
|
||||
class Create extends Component
|
||||
{
|
||||
use WithCategorySelector, WithOutletSelector, WithUpdatedData;
|
||||
|
||||
public PerfumeForm $form;
|
||||
|
||||
public array $outlets = [];
|
||||
|
||||
public array $brands = [];
|
||||
|
||||
public array $categories = [];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$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->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',
|
||||
]);
|
||||
}
|
||||
}
|
||||
63
app/Livewire/Studio/Catalog/Perfume/Edit.php
Normal file
63
app/Livewire/Studio/Catalog/Perfume/Edit.php
Normal file
@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Studio\Catalog\Perfume;
|
||||
|
||||
use App\Livewire\Forms\PerfumeForm;
|
||||
use App\Models\Brand;
|
||||
use App\Models\Category;
|
||||
use App\Models\Outlet;
|
||||
use App\Models\Perfume;
|
||||
use App\Traits\WithCategorySelector;
|
||||
use App\Traits\WithOutletSelector;
|
||||
use App\Traits\WithUpdatedData;
|
||||
use Flux\Flux;
|
||||
use Livewire\Attributes\Title;
|
||||
use Livewire\Component;
|
||||
|
||||
#[Title('Ubah Parfum')]
|
||||
class Edit extends Component
|
||||
{
|
||||
use WithCategorySelector, WithOutletSelector, WithUpdatedData;
|
||||
|
||||
public PerfumeForm $form;
|
||||
|
||||
public array $outlets = [];
|
||||
|
||||
public array $brands = [];
|
||||
|
||||
public array $categories = [];
|
||||
|
||||
public function mount(Perfume $perfume)
|
||||
{
|
||||
$this->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',
|
||||
]);
|
||||
}
|
||||
}
|
||||
37
app/Livewire/Studio/Catalog/Perfume/Index.php
Normal file
37
app/Livewire/Studio/Catalog/Perfume/Index.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Studio\Catalog\Perfume;
|
||||
|
||||
use App\Models\Perfume;
|
||||
use App\Traits\WithConfirmation;
|
||||
use Flux\Flux;
|
||||
use Livewire\Attributes\Title;
|
||||
use Livewire\Component;
|
||||
|
||||
#[Title('Parfum')]
|
||||
class Index extends Component
|
||||
{
|
||||
use WithConfirmation;
|
||||
|
||||
public function delete(Perfume $perfume)
|
||||
{
|
||||
$perfume->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',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
12
app/Models/CategoryPerfume.php
Normal file
12
app/Models/CategoryPerfume.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class CategoryPerfume extends Model
|
||||
{
|
||||
protected $table = 'category_perfume';
|
||||
|
||||
protected $guarded = ['id'];
|
||||
}
|
||||
@ -52,4 +52,9 @@ public function vouchers(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Voucher::class);
|
||||
}
|
||||
|
||||
public function perfumes(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Perfume::class);
|
||||
}
|
||||
}
|
||||
|
||||
12
app/Models/OutletPerfume.php
Normal file
12
app/Models/OutletPerfume.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class OutletPerfume extends Model
|
||||
{
|
||||
protected $table = 'outlet_perfume';
|
||||
|
||||
protected $guarded = ['id'];
|
||||
}
|
||||
52
app/Models/Perfume.php
Normal file
52
app/Models/Perfume.php
Normal file
@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\Concentration;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Spatie\MediaLibrary\HasMedia;
|
||||
use Spatie\MediaLibrary\InteractsWithMedia;
|
||||
use Spatie\Sluggable\HasSlug;
|
||||
use Spatie\Sluggable\SlugOptions;
|
||||
|
||||
class Perfume extends Model implements HasMedia
|
||||
{
|
||||
use HasFactory, HasSlug, InteractsWithMedia, SoftDeletes;
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'concentration' => 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);
|
||||
}
|
||||
}
|
||||
16
app/Traits/WithCategorySelector.php
Normal file
16
app/Traits/WithCategorySelector.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Traits;
|
||||
|
||||
trait WithCategorySelector
|
||||
{
|
||||
public function selectAllCategories()
|
||||
{
|
||||
$this->form->category_ids = array_keys($this->categories);
|
||||
}
|
||||
|
||||
public function deselectAllCategories()
|
||||
{
|
||||
$this->form->category_ids = [];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\Concentration;
|
||||
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('perfumes', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,29 @@
|
||||
<?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('category_perfume', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,30 @@
|
||||
<?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('outlet_perfume', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@ -48,6 +48,10 @@ class="bg-zinc-50 dark:bg-zinc-900 border-r rtl:border-r-0 rtl:border-l border-z
|
||||
<flux:navlist.item icon="tag" href="{{ route('studio.catalog.brand.index') }}"
|
||||
:current="request()->routeIs('studio.catalog.brand.*')" wire:navigate.hover>Merek
|
||||
</flux:navlist.item>
|
||||
|
||||
<flux:navlist.item icon="flower-2" href="{{ route('studio.catalog.perfume.index') }}"
|
||||
:current="request()->routeIs('studio.catalog.perfume.*')" wire:navigate.hover>Parfum
|
||||
</flux:navlist.item>
|
||||
</div>
|
||||
</div>
|
||||
</flux:navlist>
|
||||
|
||||
162
resources/views/livewire/studio/catalog/perfume/form.blade.php
Normal file
162
resources/views/livewire/studio/catalog/perfume/form.blade.php
Normal file
@ -0,0 +1,162 @@
|
||||
<flux:main>
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<flux:heading size="xl">{{ $pageTitle }}</flux:heading>
|
||||
</div>
|
||||
<div>
|
||||
<flux:button href="{{ route('studio.catalog.perfume.index') }}" wire:navigate.hover class="text-sm">
|
||||
Kembali
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col lg:flex-row gap-4">
|
||||
<div class="w-full lg:w-3/4 space-y-4">
|
||||
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2 items-start">
|
||||
<div class="col-span-1 lg:col-span-2">
|
||||
<flux:card class="space-y-6 p-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<flux:input label="Nama" placeholder="Masukkan nama parfum"
|
||||
wire:model.live.debounce.500ms="form.name" autofocus autocomplete="off" />
|
||||
|
||||
<flux:input label="SKU" placeholder="Masukkan stock keeping unit"
|
||||
wire:model.live.debounce.500ms="form.sku" autocomplete="off" copyable />
|
||||
|
||||
<div class="lg:col-span-2">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<flux:select variant="listbox" searchable placeholder="Pilih Merek"
|
||||
label="Merek" wire:model.live.debounce.500ms="form.brand">
|
||||
@foreach ($brands as $key => $value)
|
||||
<flux:select.option value="{{ $key }}">
|
||||
{{ $value }}
|
||||
</flux:select.option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>Harga Beli</flux:label>
|
||||
<flux:input.group>
|
||||
<flux:input.group.prefix>Rp</flux:input.group.prefix>
|
||||
<flux:input placeholder="Masukkan harga beli"
|
||||
x-mask:dynamic="$money($input, ',')"
|
||||
wire:model.live.debounce.500ms="form.cost_price"
|
||||
autocomplete="off" />
|
||||
</flux:input.group>
|
||||
<flux:error name="form.cost_price" />
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>Harga Jual</flux:label>
|
||||
<flux:input.group>
|
||||
<flux:input.group.prefix>Rp</flux:input.group.prefix>
|
||||
<flux:input placeholder="Masukkan harga jual"
|
||||
x-mask:dynamic="$money($input, ',')"
|
||||
wire:model.live.debounce.500ms="form.sale_price"
|
||||
autocomplete="off" />
|
||||
</flux:input.group>
|
||||
<flux:error name="form.sale_price" />
|
||||
</flux:field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-2">
|
||||
<flux:input label="Base Notes" placeholder="Masukkan base notes"
|
||||
wire:model.live.debounce.500ms="form.base_notes" autocomplete="off" />
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-2">
|
||||
<flux:input label="Middle Notes" placeholder="Masukkan middle notes"
|
||||
wire:model.live.debounce.500ms="form.middle_notes" autocomplete="off" />
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-2">
|
||||
<flux:input label="Top Notes" placeholder="Masukkan top notes"
|
||||
wire:model.live.debounce.500ms="form.top_notes" autocomplete="off" />
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-2">
|
||||
<flux:editor label="Deskripsi" wire:model="form.description"
|
||||
placeholder="Masukkan deskripsi" auotocomplete="off"
|
||||
class="**:data-[slot=content]:min-h-[100px]!" />
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full lg:w-1/3 space-y-4">
|
||||
<flux:card class="space-y-2 p-6">
|
||||
<flux:radio.group wire:model.live="form.concentration" variant="buttons" class="w-full *:flex-1"
|
||||
label="Concentration">
|
||||
@foreach (\App\Enums\Concentration::cases() as $item)
|
||||
<flux:radio value="{{ $item->value }}">
|
||||
{{ $item->label() }}
|
||||
</flux:radio>
|
||||
@endforeach
|
||||
</flux:radio.group>
|
||||
</flux:card>
|
||||
|
||||
<flux:card class="space-y-6 p-6">
|
||||
<flux:select variant="listbox" multiple searchable placeholder="Pilih kategori" label="Kategori"
|
||||
wire:model.live.debounce.500ms="form.category_ids">
|
||||
<flux:select.option wire:click="selectAllCategories" wire:ignore>Pilih Semua
|
||||
</flux:select.option>
|
||||
<flux:select.option wire:click="deselectAllCategories" wire:ignore>Hapus Semua
|
||||
</flux:select.option>
|
||||
@foreach ($categories as $key => $value)
|
||||
<flux:select.option value="{{ $key }}">{{ $value }}
|
||||
</flux:select.option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
</flux:card>
|
||||
|
||||
<flux:card class="space-y-6 p-6">
|
||||
<flux:select variant="listbox" multiple searchable placeholder="Pilih Outlet" label="Outlet"
|
||||
wire:model.live.debounce.500ms="form.outlet_ids">
|
||||
<flux:select.option wire:click="selectAllOutlets" wire:ignore>Pilih Semua
|
||||
</flux:select.option>
|
||||
<flux:select.option wire:click="deselectAllOutlets" wire:ignore>Hapus Semua
|
||||
</flux:select.option>
|
||||
@foreach ($outlets as $key => $value)
|
||||
<flux:select.option value="{{ $key }}">{{ $value }}
|
||||
</flux:select.option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
</flux:card>
|
||||
|
||||
<flux:card class="space-y-6 p-6">
|
||||
<div class="space-y-3">
|
||||
<h3 class="text-sm font-medium">Gambar</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>
|
||||
</flux:card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-start">
|
||||
<flux:button variant="primary" class="sm:w-auto cursor-pointer" wire:click="save">
|
||||
Simpan
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</flux:main>
|
||||
@ -0,0 +1,19 @@
|
||||
<flux:main>
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<flux:heading size="xl">{{ $pageTitle }}</flux:heading>
|
||||
</div>
|
||||
<div>
|
||||
<flux:button href="{{ route('studio.catalog.perfume.create') }}" variant="primary" wire:navigate.hover
|
||||
class="text-sm">
|
||||
Tambah
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<livewire:datatable.perfumes-table />
|
||||
</div>
|
||||
|
||||
@include('components.confirmation.delete')
|
||||
</flux:main>
|
||||
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user