feat: Implement stock opname generation, add search functionality to stock opname items, and refine quantity input handling.

This commit is contained in:
Yoga Pangestu 2026-02-23 13:47:52 +07:00
parent d4b7c9d1db
commit d2b764d3f4
7 changed files with 62 additions and 52 deletions

View File

@ -54,7 +54,8 @@ public function update(): void
continue;
}
$item->qty_physical = str_replace('.', '', (string) $qtyPhysical);
$item->qty_physical = (int) str_replace('.', '', (string) ($qtyPhysical ?? 0));
$item->save();
}
}

View File

@ -7,7 +7,9 @@
use App\Services\StockActivityLogService;
use App\Traits\Components\WithConfirmation;
use App\Traits\Components\WithToast;
use App\Traits\Utilities\WithRequestNotification;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;
use Livewire\Attributes\Title;
use Livewire\Component;
@ -15,36 +17,15 @@
#[Title('Stock Opname')]
class Index extends Component
{
use WithConfirmation, WithToast;
use WithConfirmation, WithRequestNotification, WithToast;
public $stockOpname;
public function mount(): void
public function generate(): void
{
$this->loadItems();
}
Artisan::call('stock-opname:generate');
protected function loadItems(): void
{
$this->stockOpname = StockOpname::with(['outlet', 'items'])
->where('period_month', now()->format('Y-m'))
->get()
->map(function ($stockOpname) {
$total = $stockOpname->items()->count();
$periodMonth = now()->format('Y-m');
$filled = $stockOpname->items()
->whereNotNull('qty_physical')
->count();
$percent = $total > 0 ? round(($filled / $total) * 100) : 0;
$stockOpname->total_item = $total;
$stockOpname->progress_filled = $filled;
$stockOpname->progress_percent = $percent;
$stockOpname->period_month = formatDateLocalized($stockOpname->period_month, 'F Y');
return $stockOpname;
});
$this->toast('Stock opname untuk bulan '.formatDateLocalized($periodMonth, 'F Y').' berhasil digenerate.', 'Berhasil');
}
public function requestApproval(StockOpname $stockOpname): void
@ -119,8 +100,29 @@ public function approveApproval(StockOpname $stockOpname): void
public function render(): View
{
$stockOpname = StockOpname::with(['outlet', 'items'])
->where('period_month', now()->format('Y-m'))
->get()
->map(function ($stockOpname) {
$total = $stockOpname->items()->count();
$filled = $stockOpname->items()
->whereNotNull('qty_physical')
->count();
$percent = $total > 0 ? round(($filled / $total) * 100) : 0;
$stockOpname->total_item = $total;
$stockOpname->progress_filled = $filled;
$stockOpname->progress_percent = $percent;
$stockOpname->period_month = formatDateLocalized($stockOpname->period_month, 'F Y');
return $stockOpname;
});
return view('livewire.studio.stock.stock-opname.index', [
'pageTitle' => 'Stock Opname',
'stockOpname' => $stockOpname,
]);
}
}

View File

@ -10,7 +10,6 @@
use App\Traits\Components\WithConfirmation;
use App\Traits\Components\WithToast;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Collection;
use Livewire\Attributes\Title;
use Livewire\Component;
@ -21,7 +20,7 @@ class Manage extends Component
public StockOpnameForm $form;
public Collection $items;
public string $search = '';
public function mount(StockOpname $stockOpname): void
{
@ -29,12 +28,12 @@ public function mount(StockOpname $stockOpname): void
abort(403);
}
$this->items = StockOpnameItem::with('itemable')
$items = StockOpnameItem::with('itemable')
->where('stock_opname_id', $stockOpname->id)
->get();
$outletStock = [];
foreach ($this->items as $item) {
foreach ($items as $item) {
$outletStock[$item->id] = $item->qty_physical;
}
@ -43,35 +42,32 @@ public function mount(StockOpname $stockOpname): void
$this->form->setStockOpname($stockOpname);
}
public function updated(string $propertyName, $value): void
{
$this->canOrAbort('manage stock opname');
if (str_starts_with($propertyName, 'form.outlet_stock.')) {
$itemId = explode('.', $propertyName)[2];
$item = $this->items->firstWhere('id', $itemId);
if ($item) {
$normalizedValue = str_replace('.', '', (string) $value);
$item->qty_physical = (int) $normalizedValue;
$item->difference = (int) $normalizedValue - $item->qty_system;
}
}
}
public function save(): void
{
$this->canOrAbort('manage stock opname');
$this->form->update();
$this->toast('Data stock opname berhasil disimpan.', 'Berhasil');
$this->redirectRoute('studio.stock.stock_opname.index', [
'notification' => 'Stock opname berhasil disimpan.',
], navigate: true);
}
public function render(): View
{
$items = StockOpnameItem::with('itemable')
->where('stock_opname_id', $this->form->stockOpname->id)
->when($this->search, function ($query) {
$query->whereHasMorph('itemable', '*', function ($query) {
$query->where('name', 'like', '%'.$this->search.'%')
->orWhere('display_name', 'like', '%'.$this->search.'%');
});
})
->get();
return view('livewire.studio.stock.stock-opname.manage', [
'pageTitle' => 'Kelola Stock Opname',
'items' => $items,
]);
}
}

View File

@ -19,7 +19,7 @@ protected function casts(): array
return [
'qty_system' => 'int',
'qty_physical' => 'int',
'differences' => 'int',
'difference' => 'int',
];
}

View File

@ -159,6 +159,7 @@ public function run(): void
'show stock opname',
'approve stock opname',
'manage stock opname',
'generate stock opname',
'view stock history',
'show stock history',
@ -291,6 +292,7 @@ public function run(): void
'update social settings',
'approve stock opname',
'generate stock opname',
'create tier',
'update tier',
@ -434,6 +436,7 @@ public function run(): void
'view stock opname',
'show stock opname',
'manage stock opname',
'generate stock opname',
'view testimonial',
'delete testimonial',

View File

@ -3,10 +3,17 @@
<div>
<flux:heading size="xl">{{ $pageTitle }}</flux:heading>
</div>
<div class="flex gap-2">
@can('generate stock opname')
<flux:button variant="primary" class="text-sm" wire:click="generate">
Generate
</flux:button>
@endcan
</div>
</div>
<div class="mt-6">
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4 mb-4 text-sm items-start" wire:ignore>
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4 mb-4 text-sm items-start">
@foreach ($stockOpname as $item)
<flux:card class="h-auto" wire:key="stock-opname-{{ $item->id }}">
<div class="flex justify-between items-center mb-3">

View File

@ -29,10 +29,11 @@
<flux:table.rows>
@foreach ($items as $item)
<flux:table.row wire:key="{{ $item->id }}">
<flux:table.cell>{{ $item->itemable?->display_name ?? $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>
<flux:input wire:model.live.debounce.500ms="form.outlet_stock.{{ $item->id }}"
<flux:input wire:model="form.outlet_stock.{{ $item->id }}"
x-mask:dynamic="$money($input, ',')" autocomplete="off" />
</flux:table.cell>
<flux:table.cell variant="strong">