feat: Add stock adjustment management to product, bottle, and perfume forms.
This commit is contained in:
parent
a4b83ed6cb
commit
5abf7459da
@ -6,6 +6,7 @@
|
|||||||
use App\Models\Outlet;
|
use App\Models\Outlet;
|
||||||
use App\Rules\UnsignedInteger;
|
use App\Rules\UnsignedInteger;
|
||||||
use App\Services\StockActivityLogService;
|
use App\Services\StockActivityLogService;
|
||||||
|
use App\Traits\Forms\WithFormStockAdjustment;
|
||||||
use App\Traits\Media\WithMediaHandler;
|
use App\Traits\Media\WithMediaHandler;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
@ -13,7 +14,7 @@
|
|||||||
|
|
||||||
class BottleForm extends Form
|
class BottleForm extends Form
|
||||||
{
|
{
|
||||||
use WithMediaHandler;
|
use WithFormStockAdjustment, WithMediaHandler;
|
||||||
|
|
||||||
public ?Bottle $bottle = null;
|
public ?Bottle $bottle = null;
|
||||||
|
|
||||||
@ -70,6 +71,7 @@ public function setBottle(Bottle $bottle): void
|
|||||||
$this->description = $bottle->description;
|
$this->description = $bottle->description;
|
||||||
$this->outlet_ids = $this->bottle->outlets->pluck('id')->toArray();
|
$this->outlet_ids = $this->bottle->outlets->pluck('id')->toArray();
|
||||||
$this->image = $this->mapMediaCollection($bottle->getMedia('image'));
|
$this->image = $this->mapMediaCollection($bottle->getMedia('image'));
|
||||||
|
$this->loadStockAdjustments($bottle);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function store(): void
|
public function store(): void
|
||||||
@ -129,6 +131,8 @@ public function update(): void
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->saveStockAdjustments($this->bottle, 'bottle');
|
||||||
|
|
||||||
$this->syncMedia($this->image, $this->bottle, 'image');
|
$this->syncMedia($this->image, $this->bottle, 'image');
|
||||||
$this->uploadMedia($this->image, $this->bottle, 'image');
|
$this->uploadMedia($this->image, $this->bottle, 'image');
|
||||||
});
|
});
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
use App\Models\Perfume;
|
use App\Models\Perfume;
|
||||||
use App\Rules\UnsignedInteger;
|
use App\Rules\UnsignedInteger;
|
||||||
use App\Services\StockActivityLogService;
|
use App\Services\StockActivityLogService;
|
||||||
|
use App\Traits\Forms\WithFormStockAdjustment;
|
||||||
use App\Traits\Media\WithMediaHandler;
|
use App\Traits\Media\WithMediaHandler;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
@ -14,7 +15,7 @@
|
|||||||
|
|
||||||
class PerfumeForm extends Form
|
class PerfumeForm extends Form
|
||||||
{
|
{
|
||||||
use WithMediaHandler;
|
use WithFormStockAdjustment, WithMediaHandler;
|
||||||
|
|
||||||
public ?Perfume $perfume = null;
|
public ?Perfume $perfume = null;
|
||||||
|
|
||||||
@ -96,6 +97,7 @@ public function setPerfume(Perfume $perfume): void
|
|||||||
$this->category_ids = $perfume->categories->pluck('id')->toArray();
|
$this->category_ids = $perfume->categories->pluck('id')->toArray();
|
||||||
$this->outlet_ids = $this->perfume->outlets->pluck('id')->toArray();
|
$this->outlet_ids = $this->perfume->outlets->pluck('id')->toArray();
|
||||||
$this->image = $this->mapMediaCollection($perfume->getMedia('image'));
|
$this->image = $this->mapMediaCollection($perfume->getMedia('image'));
|
||||||
|
$this->loadStockAdjustments($perfume);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function store(): void
|
public function store(): void
|
||||||
@ -163,6 +165,8 @@ public function update(): void
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->saveStockAdjustments($this->perfume, 'perfume');
|
||||||
|
|
||||||
$this->syncMedia($data['image'], $this->perfume, 'image');
|
$this->syncMedia($data['image'], $this->perfume, 'image');
|
||||||
$this->uploadMedia($data['image'], $this->perfume, 'image');
|
$this->uploadMedia($data['image'], $this->perfume, 'image');
|
||||||
});
|
});
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
use App\Models\Product;
|
use App\Models\Product;
|
||||||
use App\Rules\UnsignedInteger;
|
use App\Rules\UnsignedInteger;
|
||||||
use App\Services\StockActivityLogService;
|
use App\Services\StockActivityLogService;
|
||||||
|
use App\Traits\Forms\WithFormStockAdjustment;
|
||||||
use App\Traits\Media\WithMediaHandler;
|
use App\Traits\Media\WithMediaHandler;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
@ -13,7 +14,7 @@
|
|||||||
|
|
||||||
class ProductForm extends Form
|
class ProductForm extends Form
|
||||||
{
|
{
|
||||||
use WithMediaHandler;
|
use WithFormStockAdjustment, WithMediaHandler;
|
||||||
|
|
||||||
public ?Product $product = null;
|
public ?Product $product = null;
|
||||||
|
|
||||||
@ -67,6 +68,7 @@ public function setProduct(Product $product): void
|
|||||||
$this->description = $product->description;
|
$this->description = $product->description;
|
||||||
$this->outlet_ids = $this->product->outlets->pluck('id')->toArray();
|
$this->outlet_ids = $this->product->outlets->pluck('id')->toArray();
|
||||||
$this->image = $this->mapMediaCollection($product->getMedia('image'));
|
$this->image = $this->mapMediaCollection($product->getMedia('image'));
|
||||||
|
$this->loadStockAdjustments($product);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function store(): void
|
public function store(): void
|
||||||
@ -130,6 +132,8 @@ public function update(): void
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->saveStockAdjustments($this->product, 'product');
|
||||||
|
|
||||||
$this->syncMedia($data['image'], $this->product, 'image');
|
$this->syncMedia($data['image'], $this->product, 'image');
|
||||||
$this->uploadMedia($data['image'], $this->product, 'image');
|
$this->uploadMedia($data['image'], $this->product, 'image');
|
||||||
});
|
});
|
||||||
|
|||||||
75
app/Traits/Forms/WithFormStockAdjustment.php
Normal file
75
app/Traits/Forms/WithFormStockAdjustment.php
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Traits\Forms;
|
||||||
|
|
||||||
|
use App\Models\Outlet;
|
||||||
|
use App\Services\StockActivityLogService;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
trait WithFormStockAdjustment
|
||||||
|
{
|
||||||
|
public array $stock_adjustments = [];
|
||||||
|
|
||||||
|
protected function loadStockAdjustments(Model $item): void
|
||||||
|
{
|
||||||
|
$this->stock_adjustments = [
|
||||||
|
'outlet' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
$user = auth()->user();
|
||||||
|
$userOutletIds = $user->outlets->pluck('id')->toArray();
|
||||||
|
|
||||||
|
$itemOutlets = $item->outlets()
|
||||||
|
->whereIn('outlets.id', $userOutletIds)
|
||||||
|
->withPivot('stock')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($itemOutlets as $outlet) {
|
||||||
|
$stock = $outlet->pivot->stock ?? 0;
|
||||||
|
$this->stock_adjustments['outlet'][$outlet->id] = [
|
||||||
|
'name' => $outlet->name,
|
||||||
|
'stock' => $stock,
|
||||||
|
'new_stock' => (string) $stock,
|
||||||
|
'note' => '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function saveStockAdjustments(Model $item, string $itemType): void
|
||||||
|
{
|
||||||
|
$itemName = $item->name.($itemType === 'bottle' ? " ({$item->size}ml)" : '');
|
||||||
|
|
||||||
|
if (isset($this->stock_adjustments['outlet'])) {
|
||||||
|
foreach ($this->stock_adjustments['outlet'] as $id => $adj) {
|
||||||
|
$newStock = (int) str_replace(['.', ','], '', (string) ($adj['new_stock'] ?: '0'));
|
||||||
|
$oldStock = (int) $adj['stock'];
|
||||||
|
|
||||||
|
if ($newStock === $oldStock) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$outlet = Outlet::find($id);
|
||||||
|
if ($outlet) {
|
||||||
|
$item->outlets()->syncWithoutDetaching([$id => ['stock' => $newStock]]);
|
||||||
|
|
||||||
|
StockActivityLogService::logOutlet(
|
||||||
|
logName: 'adjustment',
|
||||||
|
event: 'update',
|
||||||
|
description: "Penyesuaian manual untuk {$itemName}: berubah dari ".formatCurrencyNumber($oldStock).' menjadi '.formatCurrencyNumber($newStock).'. Catatan: '.($adj['note'] ?: '-'),
|
||||||
|
performedOn: $item,
|
||||||
|
outlet: $outlet,
|
||||||
|
quantityChange: $newStock - $oldStock,
|
||||||
|
previousStock: $oldStock,
|
||||||
|
newStock: $newStock,
|
||||||
|
extraProperties: [
|
||||||
|
'note' => $adj['note'],
|
||||||
|
'adjustment_type' => 'manual',
|
||||||
|
'item_type' => $itemType,
|
||||||
|
'item_id' => $item->id,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
59
resources/views/components/forms/stock-management.blade.php
Normal file
59
resources/views/components/forms/stock-management.blade.php
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
@props(['form'])
|
||||||
|
|
||||||
|
<div class="mt-8 space-y-4">
|
||||||
|
<div class="flex items-center gap-2 px-1">
|
||||||
|
<flux:icon icon="map-pin" variant="outline" class="text-gray-400" size="sm" />
|
||||||
|
<flux:heading size="lg">Manajemen Stok Outlet</flux:heading>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (count($form->stock_adjustments['outlet'] ?? []) > 0)
|
||||||
|
<flux:card class="space-y-6">
|
||||||
|
<div class="flex items-center gap-2 border-b border-gray-100 dark:border-white/5 pb-4 -mx-6 px-6">
|
||||||
|
<div class="w-1.5 h-1.5 rounded-full bg-emerald-500"></div>
|
||||||
|
<flux:heading size="md" class="font-bold">Outlet Penjualan</flux:heading>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-8">
|
||||||
|
@foreach ($form->stock_adjustments['outlet'] as $id => $adj)
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span
|
||||||
|
class="text-sm font-semibold text-gray-800 dark:text-gray-200">{{ $adj['name'] }}</span>
|
||||||
|
</div>
|
||||||
|
<flux:badge color="emerald" size="sm" variant="pill" class="font-mono">Stok:
|
||||||
|
{{ number_format($adj['stock']) }}</flux:badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<flux:field>
|
||||||
|
<flux:label class="text-xs font-medium opacity-70">Penyesuaian Stok Baru</flux:label>
|
||||||
|
<flux:input.group>
|
||||||
|
<flux:input.group.prefix class="bg-gray-50/50 dark:bg-white/5">
|
||||||
|
<flux:icon icon="plus" variant="mini" />
|
||||||
|
</flux:input.group.prefix>
|
||||||
|
<flux:input type="number"
|
||||||
|
wire:model="form.stock_adjustments.outlet.{{ $id }}.new_stock"
|
||||||
|
class="font-mono" />
|
||||||
|
</flux:input.group>
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
|
<flux:field>
|
||||||
|
<flux:label class="text-xs font-medium opacity-70">Catatan Perubahan</flux:label>
|
||||||
|
<flux:input placeholder="Alasan penyesuaian..."
|
||||||
|
wire:model="form.stock_adjustments.outlet.{{ $id }}.note" autocomplete="off" />
|
||||||
|
</flux:field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@if (!$loop->last)
|
||||||
|
<flux:separator variant="subtle" />
|
||||||
|
@endif
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</flux:card>
|
||||||
|
@else
|
||||||
|
<flux:card class="p-6 text-center italic text-gray-400 text-sm">
|
||||||
|
Tidak ada outlet yang terhubung atau Anda tidak memiliki akses ke outlet manapun.
|
||||||
|
</flux:card>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
@ -63,6 +63,10 @@ class="col-span-{{ auth()->user()->hasRole(['Developer', 'Owner'])? '1': '2' }}"
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</flux:card>
|
</flux:card>
|
||||||
|
|
||||||
|
@if ($form->bottle?->exists)
|
||||||
|
<x-forms.stock-management :form="$form" />
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -87,6 +87,10 @@ class="grid grid-cols-1 md:grid-cols-{{ auth()->user()->hasRole(['Developer', 'O
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</flux:card>
|
</flux:card>
|
||||||
|
|
||||||
|
@if ($form->perfume?->exists)
|
||||||
|
<x-forms.stock-management :form="$form" />
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -58,6 +58,10 @@ class="col-span-{{ auth()->user()->hasRole(['Developer', 'Owner'])? '1': '2' }}"
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</flux:card>
|
</flux:card>
|
||||||
|
|
||||||
|
@if ($form->product?->exists)
|
||||||
|
<x-forms.stock-management :form="$form" />
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user