feat: Add custom perfume quality option and display quality in order items.

This commit is contained in:
Yoga Pangestu 2026-01-03 13:04:57 +07:00
parent 1eb43643d2
commit 9a1bd66eba
5 changed files with 180 additions and 36 deletions

View File

@ -27,15 +27,7 @@ public function updatedFormBottleId(string $value)
$formulas = Formula::where('size', $bottle->size)->orderBy('volume')->pluck('quality', 'id')->toArray();
// check if there is any formula
if (empty($formulas)) {
$this->qualities = [];
$this->toast('Kualitas tidak ditemukan, silakan hubungi Administrator.', 'Gagal', 'danger');
return;
}
$this->qualities = $formulas;
$this->qualities = $formulas + ['custom' => 'Custom'];
}
/**
@ -44,16 +36,13 @@ public function updatedFormBottleId(string $value)
*/
public function updatedFormBottleSize(string $value)
{
$formulas = Formula::where('size', $value)->orderBy('volume')->pluck('quality', 'id')->toArray();
// check if there is any formula
if (empty($formulas)) {
$this->qualities = [];
return $this->toast('Kualitas tidak ditemukan, silakan hubungi Administrator.', 'Gagal', 'danger');
if ($this->tab !== 'refill') {
return;
}
$this->qualities = $formulas;
$formulas = Formula::where('size', $value)->orderBy('volume')->pluck('quality', 'id')->toArray();
$this->qualities = $formulas + ['custom' => 'Custom'];
}
/**
@ -76,15 +65,34 @@ protected function handleNewPerfume(): void
{
$perfume = Perfume::find($this->form->perfume_id);
$bottle = Bottle::find($this->form->bottle_id);
$quality = Formula::find($this->form->quality_id);
if (! $perfume || ! $bottle || ! $quality) {
$this->toast('Parfum, botol, atau kualitas tidak valid.', 'Gagal', 'danger');
if (! $perfume || ! $bottle) {
$this->toast('Parfum atau botol tidak valid.', 'Gagal', 'danger');
return;
}
$quantity = $quality->volume;
if ($this->form->quality_id === 'custom') {
$quantity = parseRupiahToInt($this->form->bottle_size);
$qualityName = 'Custom';
if ($quantity <= 0) {
$this->toast('Jumlah parfum (Custom) tidak valid.', 'Gagal', 'danger');
return;
}
} else {
$quality = Formula::find($this->form->quality_id);
if (! $quality) {
$this->toast('Kualitas tidak valid.', 'Gagal', 'danger');
return;
}
$quantity = $quality->volume;
$qualityName = $quality->quality;
}
if ($quantity <= 0) {
$this->toast('Jumlah parfum tidak valid.', 'Gagal', 'danger');
@ -92,7 +100,7 @@ protected function handleNewPerfume(): void
return;
}
$this->addOrUpdateOrderItem($perfume, $quantity);
$this->addOrUpdateOrderItem($perfume, $quantity, $qualityName);
$this->addOrUpdateOrderItem($bottle, 1);
@ -109,23 +117,42 @@ protected function handleNewPerfume(): void
protected function handleRefillPerfume(): void
{
$perfume = Perfume::find($this->form->perfume_id);
$quality = Formula::find($this->form->quality_id);
if (! $perfume || ! $quality) {
$this->toast('Parfum, ukuran, atau kualitas tidak valid.', 'Gagal', 'danger');
if (! $perfume) {
$this->toast('Parfum tidak valid.', 'Gagal', 'danger');
return;
}
$quantity = $quality->volume;
if ($this->form->quality_id === 'custom') {
$quantity = parseRupiahToInt($this->form->bottle_size);
$qualityName = 'Custom';
if (! $perfume || ! $quality || $quantity <= 0) {
$this->toast('Parfum, ukuran, atau kualitas tidak valid.', 'Gagal', 'danger');
if ($quantity <= 0) {
$this->toast('Jumlah parfum (Custom) tidak valid.', 'Gagal', 'danger');
return;
}
} else {
$quality = Formula::find($this->form->quality_id);
if (! $quality) {
$this->toast('Kualitas tidak valid.', 'Gagal', 'danger');
return;
}
$quantity = $quality->volume;
$qualityName = $quality->quality;
}
if ($quantity <= 0) {
$this->toast('Jumlah parfum tidak valid.', 'Gagal', 'danger');
return;
}
$this->addOrUpdateOrderItem($perfume, $quantity);
$this->addOrUpdateOrderItem($perfume, $quantity, $qualityName);
$this->toast('Parfum isi ulang ditambahkan ke keranjang.', 'Berhasil');

View File

@ -11,10 +11,11 @@ trait WithManageItem
*
* @param mixed $model The model instance (Perfume, Bottle, or Product)
* @param int $quantity The quantity to add
* @param string|null $quality The quality of the item
*/
protected function addOrUpdateOrderItem($model, int $quantity): void
protected function addOrUpdateOrderItem($model, int $quantity, ?string $quality = null): void
{
$existing = $this->items->firstWhere(fn ($i) => $i->orderable_type === get_class($model) && $i->orderable_id === $model->id);
$existing = $this->items->firstWhere(fn ($i) => $i->orderable_type === get_class($model) && $i->orderable_id === $model->id && $i->quality === $quality);
if ($existing) {
$existing->increment('quantity', $quantity);
@ -24,6 +25,7 @@ protected function addOrUpdateOrderItem($model, int $quantity): void
'user_id' => auth()->id(),
'orderable_id' => $model->id,
'orderable_type' => get_class($model),
'quality' => $quality,
'cogs' => $model->cost_price ?? 0,
'quantity' => $quantity,
'unit_price' => $model->sale_price ?? 0,

View File

@ -16,6 +16,7 @@ public function up(): void
$table->foreignId('order_id')->nullable()->constrained()->cascadeOnDelete();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->morphs('orderable');
$table->string('quality', 20)->nullable();
$table->unsignedInteger('cogs');
$table->unsignedInteger('unit_price');
$table->unsignedInteger('quantity');

View File

@ -125,6 +125,18 @@
botol...</span>
</flux:field>
@endif
@if ($form->quality_id === 'custom')
<flux:field class="mt-4">
<flux:label>Jumlah (ml)</flux:label>
<flux:input.group>
<flux:input placeholder="Masukkan jumlah (ml)"
x-mask:dynamic="$money($input, ',')"
wire:model="form.bottle_size" autocomplete="off" />
<flux:input.group.suffix>ml</flux:input.group.suffix>
</flux:input.group>
</flux:field>
@endif
</div>
</div>
</flux:tab.panel>
@ -227,11 +239,18 @@
<flux:table.cell>
<flux:heading>
<div>
<div>{{ $item->orderable->name }}</div>
<span
class="text-xs text-gray-400">{{ formatCurrencyNumber($item->quantity, '') }}
x
{{ formatCurrencyNumber($item->unit_price, 'Rp') }}</span>
<div>
{{ $item->orderable->name }}
</div>
<span class="text-xs text-gray-400">
@if ($item->quality && $item->quality !== 'Custom')
{{ $item->quality }}
@else
{{ $item->quality === 'Custom' ? $item->quantity : formatCurrencyNumber($item->quantity, '') }}
x
{{ formatCurrencyNumber($item->unit_price, 'Rp') }}
@endif
</span>
</div>
</flux:heading>
</flux:table.cell>

View File

@ -91,6 +91,7 @@
'orderable_type' => Perfume::class,
'orderable_id' => $perfume->id,
'quantity' => 30,
'quality' => 'Premium',
'order_id' => null,
]);
@ -103,6 +104,100 @@
]);
});
it('can add a new perfume with custom quality', function () {
$perfume = Perfume::factory()->create(['sale_price' => 1000]);
$bottle = Bottle::factory()->create(['size' => 'Custom', 'sale_price' => 5000]);
$this->outlet->perfumes()->attach($perfume->id, ['stock' => 1000]);
$this->outlet->bottles()->attach($bottle->id, ['stock' => 50]);
Livewire::actingAs($this->user)
->test(Create::class)
->set('form.outlet_id', $this->outlet->id)
->set('tab', 'new')
->set('form.perfume_id', $perfume->id)
->set('form.bottle_id', $bottle->id)
->set('form.quality_id', 'custom')
->set('form.bottle_size', '15')
->call('addPerfume')
->assertHasNoErrors();
$this->assertDatabaseHas('order_items', [
'user_id' => $this->user->id,
'orderable_type' => Perfume::class,
'orderable_id' => $perfume->id,
'quantity' => 15,
'quality' => 'Custom',
'order_id' => null,
]);
});
it('can add a refill perfume with custom quality', function () {
$perfume = Perfume::factory()->create(['sale_price' => 1000]);
$this->outlet->perfumes()->attach($perfume->id, ['stock' => 1000]);
Livewire::actingAs($this->user)
->test(Create::class)
->set('form.outlet_id', $this->outlet->id)
->set('tab', 'refill')
->set('form.perfume_id', $perfume->id)
->set('form.quality_id', 'custom')
->set('form.bottle_size', '50')
->call('addPerfume')
->assertHasNoErrors();
$this->assertDatabaseHas('order_items', [
'user_id' => $this->user->id,
'orderable_type' => Perfume::class,
'orderable_id' => $perfume->id,
'quantity' => 50,
'quality' => 'Custom',
'order_id' => null,
]);
});
it('separates items with different qualities', function () {
$perfume = Perfume::factory()->create(['sale_price' => 1000]);
$bottle = Bottle::factory()->create(['size' => '30ml']);
$formula1 = Formula::factory()->create(['size' => '30ml', 'volume' => 30, 'quality' => 'Premium']);
$formula2 = Formula::factory()->create(['size' => '30ml', 'volume' => 30, 'quality' => 'Super']);
$this->outlet->perfumes()->attach($perfume->id, ['stock' => 1000]);
$this->outlet->bottles()->attach($bottle->id, ['stock' => 100]);
Livewire::actingAs($this->user)
->test(Create::class)
->set('form.outlet_id', $this->outlet->id)
->set('tab', 'new')
->set('form.perfume_id', $perfume->id)
->set('form.bottle_id', $bottle->id)
->set('form.quality_id', $formula1->id)
->call('addPerfume')
->set('form.perfume_id', $perfume->id)
->set('form.bottle_id', $bottle->id)
->set('form.quality_id', $formula2->id) // Switch quality
->call('addPerfume');
$this->assertDatabaseHas('order_items', [
'orderable_id' => $perfume->id,
'quality' => 'Premium',
'quantity' => 30,
]);
$this->assertDatabaseHas('order_items', [
'orderable_id' => $perfume->id,
'quality' => 'Super',
'quantity' => 30,
]);
// Bottle should have aggregated to 2 (since bottle has no quality separation)
$this->assertDatabaseHas('order_items', [
'orderable_id' => $bottle->id,
'quantity' => 2,
]);
});
it('can update item quantity in the cart', function () {
$product = Product::factory()->create(['sale_price' => 50000]);
$item = OrderItem::factory()->forProduct($product)->create([