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\Rules\UnsignedInteger;
|
||||
use App\Services\StockActivityLogService;
|
||||
use App\Traits\Forms\WithFormStockAdjustment;
|
||||
use App\Traits\Media\WithMediaHandler;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\Rule;
|
||||
@ -13,7 +14,7 @@
|
||||
|
||||
class BottleForm extends Form
|
||||
{
|
||||
use WithMediaHandler;
|
||||
use WithFormStockAdjustment, WithMediaHandler;
|
||||
|
||||
public ?Bottle $bottle = null;
|
||||
|
||||
@ -70,6 +71,7 @@ public function setBottle(Bottle $bottle): void
|
||||
$this->description = $bottle->description;
|
||||
$this->outlet_ids = $this->bottle->outlets->pluck('id')->toArray();
|
||||
$this->image = $this->mapMediaCollection($bottle->getMedia('image'));
|
||||
$this->loadStockAdjustments($bottle);
|
||||
}
|
||||
|
||||
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->uploadMedia($this->image, $this->bottle, 'image');
|
||||
});
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
use App\Models\Perfume;
|
||||
use App\Rules\UnsignedInteger;
|
||||
use App\Services\StockActivityLogService;
|
||||
use App\Traits\Forms\WithFormStockAdjustment;
|
||||
use App\Traits\Media\WithMediaHandler;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\Rule;
|
||||
@ -14,7 +15,7 @@
|
||||
|
||||
class PerfumeForm extends Form
|
||||
{
|
||||
use WithMediaHandler;
|
||||
use WithFormStockAdjustment, WithMediaHandler;
|
||||
|
||||
public ?Perfume $perfume = null;
|
||||
|
||||
@ -96,6 +97,7 @@ public function setPerfume(Perfume $perfume): void
|
||||
$this->category_ids = $perfume->categories->pluck('id')->toArray();
|
||||
$this->outlet_ids = $this->perfume->outlets->pluck('id')->toArray();
|
||||
$this->image = $this->mapMediaCollection($perfume->getMedia('image'));
|
||||
$this->loadStockAdjustments($perfume);
|
||||
}
|
||||
|
||||
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->uploadMedia($data['image'], $this->perfume, 'image');
|
||||
});
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
use App\Models\Product;
|
||||
use App\Rules\UnsignedInteger;
|
||||
use App\Services\StockActivityLogService;
|
||||
use App\Traits\Forms\WithFormStockAdjustment;
|
||||
use App\Traits\Media\WithMediaHandler;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\Rule;
|
||||
@ -13,7 +14,7 @@
|
||||
|
||||
class ProductForm extends Form
|
||||
{
|
||||
use WithMediaHandler;
|
||||
use WithFormStockAdjustment, WithMediaHandler;
|
||||
|
||||
public ?Product $product = null;
|
||||
|
||||
@ -67,6 +68,7 @@ public function setProduct(Product $product): void
|
||||
$this->description = $product->description;
|
||||
$this->outlet_ids = $this->product->outlets->pluck('id')->toArray();
|
||||
$this->image = $this->mapMediaCollection($product->getMedia('image'));
|
||||
$this->loadStockAdjustments($product);
|
||||
}
|
||||
|
||||
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->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>
|
||||
</flux:card>
|
||||
|
||||
@if ($form->bottle?->exists)
|
||||
<x-forms.stock-management :form="$form" />
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -87,6 +87,10 @@ class="grid grid-cols-1 md:grid-cols-{{ auth()->user()->hasRole(['Developer', 'O
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
@if ($form->perfume?->exists)
|
||||
<x-forms.stock-management :form="$form" />
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -58,6 +58,10 @@ class="col-span-{{ auth()->user()->hasRole(['Developer', 'Owner'])? '1': '2' }}"
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
@if ($form->product?->exists)
|
||||
<x-forms.stock-management :form="$form" />
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user