feat: Remove SKU field from products and perfumes, and introduce global ordering scopes for Product, Perfume, and Bottle models.

This commit is contained in:
Yoga Pangestu 2026-02-12 22:46:25 +07:00
parent 44576b6bf7
commit 48b2d5c6ad
23 changed files with 76 additions and 78 deletions

View File

@ -72,7 +72,6 @@ public function handle()
'brand_id' => $oldFragrance->brand_id, 'brand_id' => $oldFragrance->brand_id,
'name' => $oldFragrance->name, 'name' => $oldFragrance->name,
'slug' => Str::slug($oldFragrance->name), 'slug' => Str::slug($oldFragrance->name),
'sku' => $this->generateSKU('PERF', $oldFragrance->id),
'concentration' => Concentration::EXTRAIT_DE_PERFUME, // Default concentration 'concentration' => Concentration::EXTRAIT_DE_PERFUME, // Default concentration
'cost_price' => $oldFragrance->buying_price, 'cost_price' => $oldFragrance->buying_price,
'sale_price' => $oldFragrance->selling_price, 'sale_price' => $oldFragrance->selling_price,
@ -155,12 +154,4 @@ private function getOutletMapping(): array
return $mapping; return $mapping;
} }
private function generateSKU(string $prefix, int $id): string
{
// Use timestamp to ensure uniqueness when recreating
$timestamp = now()->format('His');
return $prefix.str_pad($id, 3, '0', STR_PAD_LEFT).$timestamp;
}
} }

View File

@ -70,7 +70,6 @@ public function handle()
$product = Product::create([ $product = Product::create([
'name' => $oldProduct->name, 'name' => $oldProduct->name,
'slug' => Str::slug($oldProduct->name), 'slug' => Str::slug($oldProduct->name),
'sku' => $this->generateSKU('PROD', $oldProduct->id),
'cost_price' => $oldProduct->buying_price, 'cost_price' => $oldProduct->buying_price,
'sale_price' => $oldProduct->selling_price, 'sale_price' => $oldProduct->selling_price,
'description' => null, 'description' => null,
@ -144,12 +143,4 @@ private function getOutletMapping(): array
return $mapping; return $mapping;
} }
private function generateSKU(string $prefix, int $id): string
{
// Use timestamp to ensure uniqueness when recreating
$timestamp = now()->format('His');
return $prefix.str_pad($id, 3, '0', STR_PAD_LEFT).$timestamp;
}
} }

View File

@ -26,13 +26,11 @@ public function columns(): array
return <<<HTML return <<<HTML
<div> <div>
<div>{$row->name}</div> <div>{$row->name}</div>
<span class="text-xs text-gray-400">{$row->sku}</span>
</div> </div>
HTML; HTML;
}) })
->searchable(function ($query, $searchTerm) { ->searchable(function ($query, $searchTerm) {
$query->where('perfumes.name', 'like', "%{$searchTerm}%") $query->where('perfumes.name', 'like', "%{$searchTerm}%");
->orWhere('sku', 'like', "%{$searchTerm}%");
}) })
->html(), ->html(),
@ -111,7 +109,6 @@ public function builder(): Builder
{ {
return Perfume::select( return Perfume::select(
'perfumes.id', 'perfumes.id',
'sku',
'perfumes.name', 'perfumes.name',
'concentration', 'concentration',
'sale_price' 'sale_price'

View File

@ -26,13 +26,11 @@ public function columns(): array
return <<<HTML return <<<HTML
<div> <div>
<div>{$row->name}</div> <div>{$row->name}</div>
<span class="text-xs text-gray-400">{$row->sku}</span>
</div> </div>
HTML; HTML;
}) })
->searchable(function ($query, $searchTerm) { ->searchable(function ($query, $searchTerm) {
$query->where('name', 'like', "%{$searchTerm}%") $query->where('name', 'like', "%{$searchTerm}%");
->orWhere('sku', 'like', "%{$searchTerm}%");
}) })
->html(), ->html(),
@ -98,7 +96,7 @@ class='flex items-center space-x-2 bg-gray-50 border border-gray-200 rounded-lg
public function builder(): Builder public function builder(): Builder
{ {
return Product::select('products.id', 'sku', 'products.name', 'sale_price') return Product::select('products.id', 'products.name', 'sale_price')
->with('outlets') ->with('outlets')
->whereHas( ->whereHas(
'outlets', 'outlets',

View File

@ -20,8 +20,6 @@ class PerfumeForm extends Form
public string $name = ''; public string $name = '';
public string $sku = '';
public ?string $brand_id = null; public ?string $brand_id = null;
public string $cost_price = ''; public string $cost_price = '';
@ -48,12 +46,6 @@ public function rules(): array
{ {
return [ return [
'name' => ['required', 'string', 'max:50'], 'name' => ['required', 'string', 'max:50'],
'sku' => [
'required',
'string',
'max:20',
Rule::unique('perfumes', 'sku')->ignore($this->perfume),
],
'brand_id' => ['nullable', Rule::exists('brands', 'id')], 'brand_id' => ['nullable', Rule::exists('brands', 'id')],
'cost_price' => [Rule::requiredIf(auth()->user()->hasRole(['Developer', 'Owner'])), new UnsignedInteger], 'cost_price' => [Rule::requiredIf(auth()->user()->hasRole(['Developer', 'Owner'])), new UnsignedInteger],
'sale_price' => ['required', new UnsignedInteger], 'sale_price' => ['required', new UnsignedInteger],
@ -93,7 +85,6 @@ public function setPerfume(Perfume $perfume): void
$this->perfume = $perfume; $this->perfume = $perfume;
$this->name = $perfume->name; $this->name = $perfume->name;
$this->sku = $perfume->sku;
$this->brand_id = $perfume->brand_id; $this->brand_id = $perfume->brand_id;
$this->cost_price = $perfume->cost_price; $this->cost_price = $perfume->cost_price;
$this->sale_price = $perfume->sale_price; $this->sale_price = $perfume->sale_price;
@ -181,7 +172,6 @@ private function prepareSavedData(): array
{ {
return [ return [
'name' => $this->name, 'name' => $this->name,
'sku' => $this->sku,
'cost_price' => parseRupiahToInt($this->cost_price == '' ? '0' : $this->cost_price), 'cost_price' => parseRupiahToInt($this->cost_price == '' ? '0' : $this->cost_price),
'sale_price' => parseRupiahToInt($this->sale_price), 'sale_price' => parseRupiahToInt($this->sale_price),
'base_notes' => $this->base_notes, 'base_notes' => $this->base_notes,

View File

@ -19,8 +19,6 @@ class ProductForm extends Form
public string $name = ''; public string $name = '';
public string $sku = '';
public string $cost_price = ''; public string $cost_price = '';
public string $sale_price = ''; public string $sale_price = '';
@ -35,12 +33,6 @@ public function rules(): array
{ {
return [ return [
'name' => ['required', 'string', 'max:50'], 'name' => ['required', 'string', 'max:50'],
'sku' => [
'required',
'string',
'max:20',
Rule::unique('products', 'sku')->ignore($this->product),
],
'cost_price' => [Rule::requiredIf(auth()->user()->hasRole(['Developer', 'Owner'])), new UnsignedInteger], 'cost_price' => [Rule::requiredIf(auth()->user()->hasRole(['Developer', 'Owner'])), new UnsignedInteger],
'sale_price' => ['required', new UnsignedInteger], 'sale_price' => ['required', new UnsignedInteger],
'description' => ['nullable', 'string'], 'description' => ['nullable', 'string'],
@ -70,7 +62,6 @@ public function setProduct(Product $product): void
$this->product = $product; $this->product = $product;
$this->name = $product->name; $this->name = $product->name;
$this->sku = $product->sku;
$this->cost_price = $product->cost_price; $this->cost_price = $product->cost_price;
$this->sale_price = $product->sale_price; $this->sale_price = $product->sale_price;
$this->description = $product->description; $this->description = $product->description;
@ -148,7 +139,6 @@ private function prepareSavedData(): array
{ {
return [ return [
'name' => $this->name, 'name' => $this->name,
'sku' => $this->sku,
'cost_price' => parseRupiahToInt($this->cost_price == '' ? '0' : $this->cost_price), 'cost_price' => parseRupiahToInt($this->cost_price == '' ? '0' : $this->cost_price),
'sale_price' => parseRupiahToInt($this->sale_price), 'sale_price' => parseRupiahToInt($this->sale_price),
'description' => $this->description, 'description' => $this->description,

View File

@ -62,7 +62,7 @@ public function mount(): void
$this->perfumes = Perfume::whereHas('outlets', fn ($query) => $query->where('outlets.id', $this->form->outlet_id))->orderBy('name')->pluck('name', 'id')->toArray(); $this->perfumes = Perfume::whereHas('outlets', fn ($query) => $query->where('outlets.id', $this->form->outlet_id))->orderBy('name')->pluck('name', 'id')->toArray();
$this->bottles = Bottle::whereHas('outlets', fn ($query) => $query->where('outlets.id', $this->form->outlet_id))->orderBy('size')->pluck('name', 'id')->toArray(); $this->bottles = Bottle::whereHas('outlets', fn ($query) => $query->where('outlets.id', $this->form->outlet_id))->get()->pluck('display_name', 'id')->toArray();
$this->products = Product::whereHas('outlets', fn ($query) => $query->where('outlets.id', $this->form->outlet_id))->orderBy('name')->pluck('name', 'id')->toArray(); $this->products = Product::whereHas('outlets', fn ($query) => $query->where('outlets.id', $this->form->outlet_id))->orderBy('name')->pluck('name', 'id')->toArray();
@ -113,7 +113,7 @@ public function save(): void
public function updatedFormOutletId($value): void public function updatedFormOutletId($value): void
{ {
$this->perfumes = Perfume::whereHas('outlets', fn ($query) => $query->where('outlets.id', $value))->orderBy('name')->pluck('name', 'id')->toArray(); $this->perfumes = Perfume::whereHas('outlets', fn ($query) => $query->where('outlets.id', $value))->orderBy('name')->pluck('name', 'id')->toArray();
$this->bottles = Bottle::whereHas('outlets', fn ($query) => $query->where('outlets.id', $value))->orderBy('size')->pluck('name', 'id')->toArray(); $this->bottles = Bottle::whereHas('outlets', fn ($query) => $query->where('outlets.id', $value))->get()->pluck('display_name', 'id')->toArray();
$this->products = Product::whereHas('outlets', fn ($query) => $query->where('outlets.id', $value))->orderBy('name')->pluck('name', 'id')->toArray(); $this->products = Product::whereHas('outlets', fn ($query) => $query->where('outlets.id', $value))->orderBy('name')->pluck('name', 'id')->toArray();
} }

View File

@ -34,7 +34,7 @@ class Formula extends Component
public function mount(): void public function mount(): void
{ {
$this->sizes = Bottle::distinct()->orderBy('size')->pluck('size')->toArray(); $this->sizes = Bottle::distinct()->get()->pluck('size')->toArray();
} }
public function create(): void public function create(): void

View File

@ -43,7 +43,7 @@ public function mount(): void
$this->perfumes = Perfume::orderBy('name')->pluck('name', 'id')->toArray(); $this->perfumes = Perfume::orderBy('name')->pluck('name', 'id')->toArray();
$this->bottles = Bottle::orderBy('name')->pluck('name', 'id')->toArray(); $this->bottles = Bottle::get()->pluck('display_name', 'id')->toArray();
$this->products = Product::orderBy('name')->pluck('name', 'id')->toArray(); $this->products = Product::orderBy('name')->pluck('name', 'id')->toArray();

View File

@ -76,23 +76,20 @@ private function loadCreateItems(): void
$this->perfumes = $warehouse->perfumes() $this->perfumes = $warehouse->perfumes()
->wherePivot('stock', '>', 0) ->wherePivot('stock', '>', 0)
->orderBy('name')
->get() ->get()
->pluck('name', 'id') ->pluck('name', 'id')
->toArray(); ->toArray();
$this->products = $warehouse->products() $this->products = $warehouse->products()
->wherePivot('stock', '>', 0) ->wherePivot('stock', '>', 0)
->orderBy('name')
->get() ->get()
->pluck('name', 'id') ->pluck('name', 'id')
->toArray(); ->toArray();
$this->bottles = $warehouse->bottles() $this->bottles = $warehouse->bottles()
->wherePivot('stock', '>', 0) ->wherePivot('stock', '>', 0)
->orderBy('name')
->get() ->get()
->pluck('name', 'id') ->pluck('display_name', 'id')
->toArray(); ->toArray();
} }

View File

@ -76,23 +76,20 @@ private function loadCreateItems(): void
$this->perfumes = $sourceOutlet->perfumes() $this->perfumes = $sourceOutlet->perfumes()
->wherePivot('stock', '>', 0) ->wherePivot('stock', '>', 0)
->orderBy('name')
->get() ->get()
->pluck('name', 'id') ->pluck('name', 'id')
->toArray(); ->toArray();
$this->products = $sourceOutlet->products() $this->products = $sourceOutlet->products()
->wherePivot('stock', '>', 0) ->wherePivot('stock', '>', 0)
->orderBy('name')
->get() ->get()
->pluck('name', 'id') ->pluck('name', 'id')
->toArray(); ->toArray();
$this->bottles = $sourceOutlet->bottles() $this->bottles = $sourceOutlet->bottles()
->wherePivot('stock', '>', 0) ->wherePivot('stock', '>', 0)
->orderBy('name')
->get() ->get()
->pluck('name', 'id') ->pluck('display_name', 'id')
->toArray(); ->toArray();
} }

View File

@ -2,6 +2,7 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\BelongsToMany;
@ -19,6 +20,13 @@ class Bottle extends Model implements HasMedia
protected $guarded = ['id']; protected $guarded = ['id'];
protected static function booted(): void
{
static::addGlobalScope('orderedBySize', function ($builder) {
$builder->orderBy('size', 'asc')->orderBy('name', 'asc');
});
}
protected function casts(): array protected function casts(): array
{ {
return [ return [
@ -35,6 +43,13 @@ public function getSlugOptions(): SlugOptions
->saveSlugsTo('slug'); ->saveSlugsTo('slug');
} }
protected function displayName(): Attribute
{
return Attribute::make(
get: fn () => "{$this->size}ml {$this->name}",
);
}
public function items(): MorphMany public function items(): MorphMany
{ {
return $this->morphMany(OrderItem::class, 'orderable'); return $this->morphMany(OrderItem::class, 'orderable');

View File

@ -21,6 +21,13 @@ class Perfume extends Model implements HasMedia
protected $guarded = ['id']; protected $guarded = ['id'];
protected static function booted(): void
{
static::addGlobalScope('alphabetical', function ($builder) {
$builder->orderBy('name', 'asc');
});
}
protected function casts(): array protected function casts(): array
{ {
return [ return [

View File

@ -19,6 +19,13 @@ class Product extends Model implements HasMedia
protected $guarded = ['id']; protected $guarded = ['id'];
protected static function booted(): void
{
static::addGlobalScope('alphabetical', function ($builder) {
$builder->orderBy('name', 'asc');
});
}
protected function casts(): array protected function casts(): array
{ {
return [ return [

View File

@ -20,7 +20,6 @@ public function definition(): array
'brand_id' => Brand::factory(), 'brand_id' => Brand::factory(),
'name' => Str::title($name), 'name' => Str::title($name),
'slug' => Str::slug($name.'-'.$this->faker->unique()->randomNumber()), 'slug' => Str::slug($name.'-'.$this->faker->unique()->randomNumber()),
'sku' => strtoupper($this->faker->unique()->bothify('PER-#####')),
'concentration' => $this->faker->randomElement(Concentration::cases()), 'concentration' => $this->faker->randomElement(Concentration::cases()),
'cost_price' => $this->faker->numberBetween(50000, 250000), 'cost_price' => $this->faker->numberBetween(50000, 250000),
'sale_price' => $this->faker->numberBetween(80000, 400000), 'sale_price' => $this->faker->numberBetween(80000, 400000),

View File

@ -17,7 +17,6 @@ public function definition(): array
return [ return [
'name' => Str::title($name), 'name' => Str::title($name),
'slug' => Str::slug($name.'-'.$this->faker->unique()->randomNumber()), 'slug' => Str::slug($name.'-'.$this->faker->unique()->randomNumber()),
'sku' => strtoupper($this->faker->unique()->bothify('PRD-#####')),
'cost_price' => $this->faker->numberBetween(20000, 150000), 'cost_price' => $this->faker->numberBetween(20000, 150000),
'sale_price' => $this->faker->numberBetween(30000, 200000), 'sale_price' => $this->faker->numberBetween(30000, 200000),
]; ];

View File

@ -0,0 +1,36 @@
<?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::table('perfumes', function (Blueprint $table) {
$table->dropColumn('sku');
});
Schema::table('products', function (Blueprint $table) {
$table->dropColumn('sku');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('perfumes', function (Blueprint $table) {
$table->string('sku', 20)->unique()->after('slug');
});
Schema::table('products', function (Blueprint $table) {
$table->string('sku', 20)->unique()->after('slug');
});
}
};

View File

@ -32,7 +32,6 @@ public function run(): void
foreach ($perfumeNames as $index => $name) { foreach ($perfumeNames as $index => $name) {
$brand = $brands[$index % $brands->count()]; $brand = $brands[$index % $brands->count()];
$slug = Str::slug($name); $slug = Str::slug($name);
$sku = Str::upper(Str::random(3)).'-'.str_pad($index + 1, 4, '0', STR_PAD_LEFT);
$salePrice = fake()->numberBetween(25, 50) * 100; $salePrice = fake()->numberBetween(25, 50) * 100;
@ -40,7 +39,6 @@ public function run(): void
'brand_id' => $brand->id, 'brand_id' => $brand->id,
'name' => $name, 'name' => $name,
'slug' => $slug, 'slug' => $slug,
'sku' => $sku,
'concentration' => fake()->randomElement(Concentration::values()), 'concentration' => fake()->randomElement(Concentration::values()),
'cost_price' => fake()->numberBetween(5, 11) * 100, 'cost_price' => fake()->numberBetween(5, 11) * 100,
'sale_price' => $salePrice, 'sale_price' => $salePrice,

View File

@ -26,14 +26,12 @@ public function run(): void
foreach ($products as $index => $name) { foreach ($products as $index => $name) {
$slug = Str::slug($name); $slug = Str::slug($name);
$sku = Str::upper(Str::random(3)).'-'.str_pad($index + 1, 4, '0', STR_PAD_LEFT);
$salePrice = fake()->numberBetween(15, 200) * 1000; $salePrice = fake()->numberBetween(15, 200) * 1000;
$product = Product::create([ $product = Product::create([
'name' => $name, 'name' => $name,
'slug' => $slug, 'slug' => $slug,
'sku' => $sku,
'cost_price' => fake()->numberBetween(15, 80) * 1000, 'cost_price' => fake()->numberBetween(15, 80) * 1000,
'sale_price' => $salePrice, 'sale_price' => $salePrice,
'description' => fake()->sentence(12), 'description' => fake()->sentence(12),

View File

@ -25,12 +25,6 @@
<flux:error name="form.name" /> <flux:error name="form.name" />
</flux:field> </flux:field>
<flux:field>
<flux:label>SKU <span class="text-red-500 ms-1">*</span></flux:label>
<flux:input placeholder="001" wire:model="form.sku" autocomplete="off"
copyable />
<flux:error name="form.sku" />
</flux:field>
<div class="lg:col-span-2"> <div class="lg:col-span-2">
<div <div

View File

@ -25,12 +25,6 @@
<flux:error name="form.name" /> <flux:error name="form.name" />
</flux:field> </flux:field>
<flux:field>
<flux:label>SKU <span class="text-red-500 ms-1">*</span></flux:label>
<flux:input placeholder="001" wire:model="form.sku" autocomplete="off"
copyable />
<flux:error name="form.sku" />
</flux:field>
@if (auth()->user()->hasRole(['Developer', 'Owner'])) @if (auth()->user()->hasRole(['Developer', 'Owner']))
<flux:field> <flux:field>

View File

@ -29,7 +29,7 @@
<flux:table.rows> <flux:table.rows>
@foreach ($items as $item) @foreach ($items as $item)
<flux:table.row wire:key="{{ $item->id }}"> <flux:table.row wire:key="{{ $item->id }}">
<flux:table.cell>{{ $item->itemable?->name }}</flux:table.cell> <flux:table.cell>{{ $item->itemable?->display_name ?? $item->itemable?->name }}</flux:table.cell>
<flux:table.cell>{{ $item->qty_system }}</flux:table.cell> <flux:table.cell>{{ $item->qty_system }}</flux:table.cell>
<flux:table.cell> <flux:table.cell>
<flux:input wire:model.live.debounce.500ms="form.outlet_stock.{{ $item->id }}" <flux:input wire:model.live.debounce.500ms="form.outlet_stock.{{ $item->id }}"

View File

@ -24,7 +24,7 @@
<flux:table.rows> <flux:table.rows>
@foreach ($items as $item) @foreach ($items as $item)
<flux:table.row wire:key="{{ $item->id }}"> <flux:table.row wire:key="{{ $item->id }}">
<flux:table.cell>{{ $item->itemable?->name }}</flux:table.cell> <flux:table.cell>{{ $item->itemable?->display_name ?? $item->itemable?->name }}</flux:table.cell>
<flux:table.cell>{{ $item->qty_system }}</flux:table.cell> <flux:table.cell>{{ $item->qty_system }}</flux:table.cell>
<flux:table.cell>{{ $item->qty_physical }}</flux:table.cell> <flux:table.cell>{{ $item->qty_physical }}</flux:table.cell>
<flux:table.cell variant="strong"> <flux:table.cell variant="strong">