feat: add stock adjustment functionality for warehouse items via a modal form.

This commit is contained in:
Yoga Pangestu 2026-02-12 14:27:40 +07:00
parent 5b8f89f693
commit 6477684540
3 changed files with 198 additions and 14 deletions

View File

@ -0,0 +1,97 @@
<?php
namespace App\Livewire\Forms\Studio\Master\Warehouse;
use App\Models\Warehouse;
use App\Services\StockActivityLogService;
use Livewire\Form;
class AdjustmentStockForm extends Form
{
public string $itemId = '';
public string $type = '';
public string $name = '';
public string $oldStock = '0';
public string $newStock = '0';
public ?string $note = null;
public function rules(): array
{
return [
'newStock' => 'required',
'note' => 'nullable',
];
}
public function validationAttributes(): array
{
return [
'newStock' => 'stok baru',
'note' => 'keterangan',
];
}
public function setAdjustment(string $itemId, string $type, string $name, int $stock): void
{
$this->itemId = $itemId;
$this->type = $type;
$this->name = $name;
$this->oldStock = formatCurrencyNumber($stock);
$this->newStock = formatCurrencyNumber($stock);
$this->note = null;
}
public function save(Warehouse $warehouse): void
{
$this->validate();
$newStockValue = (int) str_replace(['.', ','], '', (string) $this->newStock);
$oldStockValue = (int) str_replace(['.', ','], '', (string) $this->oldStock);
$typeId = explode('_', $this->itemId);
$type = $typeId[0];
$id = $typeId[1];
$relation = match ($type) {
'perfume' => 'perfumes',
'product' => 'products',
'bottle' => 'bottles',
default => null,
};
if (! $relation) {
return;
}
$item = $warehouse->{$relation}()->find($id);
if (! $item) {
return;
}
$warehouse->{$relation}()->updateExistingPivot($id, [
'stock' => $newStockValue,
]);
// Log the activity
StockActivityLogService::logWarehouse(
logName: 'adjustment',
event: 'update',
description: "Manual adjustment for {$this->name}: changed from {$oldStockValue} to {$newStockValue}. Note: ".($this->note ?? '-'),
performedOn: $item,
warehouse: $warehouse,
quantityChange: $newStockValue - $oldStockValue,
previousStock: $oldStockValue,
newStock: $newStockValue,
extraProperties: [
'note' => $this->note,
'adjustment_type' => 'manual',
]
);
}
}

View File

@ -2,16 +2,26 @@
namespace App\Livewire\Studio\Master\Warehouse; namespace App\Livewire\Studio\Master\Warehouse;
use App\Livewire\Forms\Studio\Master\Warehouse\AdjustmentStockForm;
use App\Models\Warehouse; use App\Models\Warehouse;
use App\Traits\Authorization\WithAuthorization; use App\Traits\Authorization\WithAuthorization;
use App\Traits\Components\WithCloseModal;
use App\Traits\Components\WithConfirmation;
use App\Traits\Components\WithToast;
use App\Traits\Notification\WithSubscribeNotification;
use App\Traits\Utilities\WithUpdatedData;
use Flux\Flux;
use Illuminate\Contracts\View\View; use Illuminate\Contracts\View\View;
use Livewire\Attributes\On;
use Livewire\Attributes\Title; use Livewire\Attributes\Title;
use Livewire\Component; use Livewire\Component;
#[Title('Detail Gudang')] #[Title('Detail Gudang')]
class Show extends Component class Show extends Component
{ {
use WithAuthorization; use WithAuthorization, WithCloseModal, WithConfirmation, WithSubscribeNotification, WithToast, WithUpdatedData;
public AdjustmentStockForm $form;
public Warehouse $warehouse; public Warehouse $warehouse;
@ -19,6 +29,10 @@ class Show extends Component
public array $type = []; public array $type = [];
public string $method = 'saveAdjustment';
public string $modalTitle = '';
public function mount(Warehouse $warehouse): void public function mount(Warehouse $warehouse): void
{ {
$this->warehouse = $warehouse; $this->warehouse = $warehouse;
@ -121,6 +135,29 @@ public function getItemsProperty()
return $perfumes->concat($products)->concat($bottles); return $perfumes->concat($products)->concat($bottles);
} }
#[On('modal:open')]
public function openModal(string $method, string $modalTitle, string $itemId, string $type, string $name, int $stock): void
{
$this->resetValidation();
$this->resetErrorBag();
$this->method = $method;
$this->modalTitle = $modalTitle;
$this->form->setAdjustment($itemId, $type, $name, $stock);
}
public function saveAdjustment(): void
{
$this->canOrAbort('manage adjustment');
$this->form->save($this->warehouse);
$this->toast('Stok berhasil diperbarui.');
Flux::modals()->close();
}
public function render(): View public function render(): View
{ {
return view('livewire.studio.master.warehouse.show', [ return view('livewire.studio.master.warehouse.show', [

View File

@ -46,9 +46,9 @@
<flux:table.column>Tipe</flux:table.column> <flux:table.column>Tipe</flux:table.column>
<flux:table.column>Item</flux:table.column> <flux:table.column>Item</flux:table.column>
<flux:table.column>Stok</flux:table.column> <flux:table.column>Stok</flux:table.column>
@can('audit stock warehouse') @canAny(['manage adjustment', 'audit stock warehouse'])
<flux:table.column>Aksi</flux:table.column> <flux:table.column>Aksi</flux:table.column>
@endcan @endcanAny
</flux:table.columns> </flux:table.columns>
@ -57,22 +57,35 @@
<flux:table.row :key="$item['id']"> <flux:table.row :key="$item['id']">
<flux:table.cell> <flux:table.cell>
<flux:badge size="sm" <flux:badge size="sm"
:color="$item['type'] === 'Parfum' ? 'fuchsia' : ($item['type'] === 'Produk' ? 'cyan' : :color="$item['type'] === 'Parfum' ? 'fuchsia' : ($item['type'] === 'Produk' ?
'cyan' :
'zinc')"> 'zinc')">
{{ $item['type'] }}</flux:badge> {{ $item['type'] }}</flux:badge>
</flux:table.cell> </flux:table.cell>
<flux:table.cell class="font-medium">{{ $item['name'] }}</flux:table.cell> <flux:table.cell class="font-medium">{{ $item['name'] }}</flux:table.cell>
<flux:table.cell>{{ number_format($item['stock'], 0, ',', '.') }}</flux:table.cell> <flux:table.cell>{{ number_format($item['stock'], 0, ',', '.') }}</flux:table.cell>
<flux:table.cell> @canAny(['manage adjustment', 'audit stock warehouse'])
@can('audit stock warehouse') <flux:table.cell>
<flux:tooltip content="Audit"> @can('audit stock warehouse')
<flux:button size="sm" variant="primary" icon="clipboard-document-list" <flux:tooltip content="Audit">
href="{{ route('studio.master.warehouse.' . $item['type_key'] . '.audit', ['warehouse' => $warehouse->hash, $item['type_key'] => $item['hash_id']]) }}" <flux:button size="sm" variant="primary" icon="clipboard-document-list"
wire:navigate> href="{{ route('studio.master.warehouse.' . $item['type_key'] . '.audit', ['warehouse' => $warehouse->hash, $item['type_key'] => $item['hash_id']]) }}"
</flux:button> wire:navigate>
</flux:tooltip> </flux:button>
@endcan </flux:tooltip>
</flux:table.cell> @endcan
@can('manage adjustment')
<flux:modal.trigger name="form-modal">
<flux:tooltip content="Ubah">
<flux:button variant="primary" color="yellow" icon="pencil-square"
size="sm"
wire:click="$dispatch('modal:open', { method: 'saveAdjustment', modalTitle: 'Ubah Stok', itemId: '{{ $item['id'] }}', type: '{{ $item['type'] }}', name: '{{ $item['name'] }}', stock: {{ $item['stock'] }} })">
</flux:button>
</flux:tooltip>
</flux:modal.trigger>
@endcan
</flux:table.cell>
@endcanAny
</flux:table.row> </flux:table.row>
@empty @empty
<flux:table.row> <flux:table.row>
@ -85,4 +98,41 @@
</flux:table> </flux:table>
</flux:card> </flux:card>
</div> </div>
<flux:modal name="form-modal" class="w-[95%] max-w-sm md:max-w-xl mx-auto" @close="closeModal('form-modal')">
<div class="p-4 space-y-6">
<div>
<flux:heading size="lg">{{ $modalTitle }}</flux:heading>
<flux:subheading>Sesuaikan stok untuk {{ $form?->name ?? '-' }} ({{ $form?->type ?? '-' }})
</flux:subheading>
</div>
<div class="grid grid-cols-2 gap-4">
<flux:field>
<flux:label>Stok Saat Ini</flux:label>
<flux:input wire:model="form.oldStock" disabled />
</flux:field>
<flux:field>
<flux:label>Stok Baru <span class="text-red-500 ms-1">*</span></flux:label>
<flux:input placeholder="Masukkan stok" x-mask:dynamic="$money($input, ',')"
wire:model="form.newStock" autocomplete="off" />
<flux:error name="form.newStock" />
</flux:field>
</div>
<flux:field>
<flux:label>Keterangan</flux:label>
<flux:textarea placeholder="Contoh: Koreksi stok fisik, Rusak, dsb." wire:model="form.note" />
<flux:error name="form.note" />
</flux:field>
<div class="flex gap-2">
<flux:spacer />
<flux:button variant="primary" wire:click="{{ $method }}">
Simpan
</flux:button>
</div>
</div>
</flux:modal>
</flux:main> </flux:main>