feat(stock opname): implementasi module/fitur stock opname

This commit is contained in:
Yoga Pangestu 2025-11-24 17:02:25 +07:00
parent 87ae5538b3
commit 9b25af9ef0
22 changed files with 857 additions and 9 deletions

View File

@ -0,0 +1,53 @@
<?php
namespace App\Console\Commands;
use App\Models\Outlet;
use App\Models\StockOpname;
use App\Models\StockOpnameItem;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class GenerateStockOpname extends Command
{
protected $signature = 'stock-opname:generate';
protected $description = 'Automatically generate a stock opname document';
public function handle()
{
$periodMonth = now()->format('Y-m');
$outlets = Outlet::query()->operational()->get();
$user = User::role('Developer')->first();
DB::transaction(function () use ($outlets, $periodMonth, $user) {
foreach ($outlets as $outlet) {
$stockOpname = StockOpname::firstOrCreate([
'outlet_id' => $outlet->id,
'period_month' => $periodMonth,
]);
$items = collect()
->merge($outlet->perfumes)
->merge($outlet->bottles)
->merge($outlet->products);
foreach ($items as $item) {
StockOpnameItem::firstOrCreate([
'stock_opname_id' => $stockOpname->id,
'itemable_type' => get_class($item),
'itemable_id' => $item->id,
], [
'user_id' => $user->id,
'qty_system' => $item->pivot?->stock,
]);
}
}
});
$this->info("Stock opname untuk bulan {$periodMonth} berhasil digenerate.");
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Enums;
use App\Traits\WithCommentEnum;
use App\Traits\WithValueEnum;
enum StockOpnameStatus: int
{
use WithCommentEnum, WithValueEnum;
case DRAFT = 1;
case PROCESS = 2;
case PENDING_APPROVAL = 3;
case COMPLETED = 4;
public function label()
{
return match ($this) {
self::DRAFT => 'Draft',
self::PROCESS => 'Proses',
self::PENDING_APPROVAL => 'Menunggu Persetujuan',
self::COMPLETED => 'Selesai',
};
}
public function color()
{
return match ($this) {
self::DRAFT => 'yellow',
self::PROCESS => 'teal',
self::PENDING_APPROVAL => 'orange',
self::COMPLETED => 'emerald',
};
}
}

View File

@ -95,8 +95,8 @@ public function columns(): array
$route = route('studio.catalog.bottle.audit', [$value['id'], $value['bottle_id']]);
$canAudit = auth()->user()->can('audit bottle');
$tag = $canAudit ? 'div' : 'a';
$attrs = $canAudit ? '' : 'href="'.$route.'" wire:navigate';
$tag = ! $canAudit ? 'div' : 'a';
$attrs = ! $canAudit ? '' : 'href="'.$route.'" wire:navigate';
return Blade::render("
<{$tag} {$attrs}

View File

@ -98,8 +98,8 @@ public function columns(): array
$route = route('studio.catalog.perfume.audit', [$value['id'], $value['perfume_id']]);
$canAudit = auth()->user()->can('audit perfume');
$tag = $canAudit ? 'div' : 'a';
$attrs = $canAudit ? '' : 'href="'.$route.'" wire:navigate';
$tag = ! $canAudit ? 'div' : 'a';
$attrs = ! $canAudit ? '' : 'href="'.$route.'" wire:navigate';
return Blade::render("
<{$tag} {$attrs}

View File

@ -87,8 +87,8 @@ public function columns(): array
$route = route('studio.catalog.product.audit', [$value['id'], $value['product_id']]);
$canAudit = auth()->user()->can('audit product');
$tag = $canAudit ? 'div' : 'a';
$attrs = $canAudit ? '' : 'href="'.$route.'" wire:navigate';
$tag = ! $canAudit ? 'div' : 'a';
$attrs = ! $canAudit ? '' : 'href="'.$route.'" wire:navigate';
return Blade::render("
<{$tag} {$attrs}

View File

@ -0,0 +1,56 @@
<?php
namespace App\Livewire\Datatable\Studio\Manage;
use App\Models\StockOpname;
use App\Traits\Datatable\WithAppendColumn;
use App\Traits\Datatable\WithConfiguration;
use App\Traits\Datatable\WithPrependColumn;
use App\Traits\WithMediaHandler;
use Illuminate\Database\Eloquent\Builder;
use Rappasoft\LaravelLivewireTables\DataTableComponent;
use Rappasoft\LaravelLivewireTables\Views\Column;
class StockOpnameTable extends DataTableComponent
{
use WithAppendColumn, WithConfiguration, WithMediaHandler, WithPrependColumn;
protected $model = StockOpname::class;
public function columns(): array
{
return [
Column::make('Outlet', 'outlet.name')->searchable(),
Column::make('Periode Bulan', 'period_month')
->format(fn ($row) => formatDate($row, 'F Y'))
->searchable()
->sortable(),
Column::make('Aksi')
->label(function ($row) {
$actions = '';
if (auth()->user()->can('show stock opname')) {
$actions .= view('components.datatables.new-page-action', [
'label' => 'Lihat',
'route' => route('studio.manage.stock_opname.show', ['stockOpname', $row->hash]),
'buttonColor' => 'blue',
'buttonIcon' => 'eye',
])->render();
}
return $actions;
})
->html()
->hideIf(auth()->user()->cannot('show stock opname')),
];
}
public function builder(): Builder
{
return StockOpname::select('stock_opname.id', 'period_month')
->where('period_month', '!=', now()->format('Y-m'))
->with('outlet');
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace App\Livewire\Forms\Studio\Manage;
use App\Models\StockOpname;
use App\Models\StockOpnameItem;
use App\Rules\UnsignedInteger;
use Livewire\Form;
class StockOpnameForm extends Form
{
public StockOpname $stockOpname;
public array $outlet_stock = [];
public ?string $note = null;
public function rules()
{
return [
'outlet_stock' => 'array',
'outlet_stock.*' => ['nullable', new UnsignedInteger],
'note' => 'nullable',
];
}
public function validationAttributes()
{
return [
'outlet_stock' => 'stock outlet',
'outlet_stock.*' => 'stock outlet',
'note' => 'catatan',
];
}
public function setStockOpname(StockOpname $stockOpname)
{
$stockOpname->load('items');
$this->stockOpname = $stockOpname;
$this->note = $stockOpname->note;
}
public function update()
{
$this->validate();
$this->stockOpname->update(['note' => $this->note]);
foreach ($this->outlet_stock as $itemId => $qtyPhysical) {
$item = StockOpnameItem::find($itemId);
if (! $item) {
continue;
}
$item->qty_physical = $qtyPhysical;
$item->save();
}
}
}

View File

@ -0,0 +1,136 @@
<?php
namespace App\Livewire\Studio\Manage\StockOpname;
use App\Enums\StockOpnameStatus;
use App\Models\StockOpname;
use App\Traits\WithConfirmation;
use App\Traits\WithToast;
use Illuminate\Support\Facades\DB;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Title('Stock Opname')]
class Index extends Component
{
use WithConfirmation, WithToast;
public $stockOpname;
public function mount()
{
$this->loadItems();
}
protected function loadItems()
{
$this->stockOpname = StockOpname::with(['outlet', 'items'])
->where('period_month', now()->format('Y-m'))
->get()
->map(function ($stockOpname) {
$outlet = $stockOpname->outlet;
$parfumCount = $outlet->perfumes()->count();
$botolCount = $outlet->bottles()->count();
$produkCount = $outlet->products()->count();
$total = $parfumCount + $botolCount + $produkCount;
$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 = formatDate($stockOpname->period_month, 'F Y');
return $stockOpname;
});
}
public function requestApproval(StockOpname $stockOpname)
{
$stockOpname->update(['status' => StockOpnameStatus::PENDING_APPROVAL]);
$this->toast('Permohonan persetujuan berhasil diajukan.', 'Berhasil');
$this->redirectRoute('studio.manage.stock_opname.index', navigate: true);
}
public function approveApproval(StockOpname $stockOpname)
{
DB::transaction(function () use ($stockOpname) {
$stockOpname->update(['status' => StockOpnameStatus::COMPLETED]);
$outlet = $stockOpname->outlet;
foreach ($stockOpname->items as $item) {
if (is_null($item->qty_physical)) {
continue;
}
$itemable = $item->itemable;
$morphType = class_basename($itemable);
$relationMap = [
'Perfume' => 'perfumes',
'Product' => 'products',
'Bottle' => 'bottles',
];
if (! isset($relationMap[$morphType])) {
continue;
}
$relationName = $relationMap[$morphType];
$currentStock = $outlet->$relationName()
->where($itemable->getTable().'.id', $itemable->id)
->first()?->pivot?->stock ?? 0;
$quantity = $item->qty_physical;
$outlet->$relationName()->syncWithoutDetaching([
$itemable->id => ['stock' => $quantity],
]);
activity('Stock Opname')
->performedOn($itemable)
->causedBy(auth()->user())
->withProperties([
'itemable_id' => $item->itemable_id,
'itemable_type' => $item->itemable_type,
'outlet_id' => $outlet->id,
'quantity_change' => $quantity,
'previous_stock' => $currentStock,
'new_stock' => $currentStock + $quantity,
'itemable_id' => $item->id ?? null,
])
->event('Mengubah')
->log(sprintf(
'Stok %s di %s diperbarui dari %s menjadi %s',
$itemable->name,
$outlet->name,
$currentStock,
$quantity
));
}
});
$this->toast('Stock opname berhasil disetujui dan stok outlet disesuaikan.', 'Berhasil');
$this->redirectRoute('studio.manage.stock_opname.index', navigate: true);
}
public function render()
{
return view('livewire.studio.manage.stock-opname.index', [
'pageTitle' => 'Stock Opname',
]);
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace App\Livewire\Studio\Manage\StockOpname;
use App\Enums\StockOpnameStatus;
use App\Livewire\Forms\Studio\Manage\StockOpnameForm;
use App\Models\StockOpname;
use App\Models\StockOpnameItem;
use App\Traits\WithAuthorization;
use App\Traits\WithConfirmation;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Title('Kelola Stock Opname')]
class Manage extends Component
{
use WithAuthorization, WithConfirmation;
public StockOpnameForm $form;
public $items;
public function mount(StockOpname $stockOpname)
{
if ($stockOpname->status !== StockOpnameStatus::PROCESS) {
abort(403);
}
$this->items = StockOpnameItem::with('itemable')
->where('stock_opname_id', $stockOpname->id)
->get();
$outletStock = [];
foreach ($this->items as $item) {
$outletStock[$item->id] = $item->qty_physical;
}
$this->form->outlet_stock = $outletStock;
$this->form->setStockOpname($stockOpname);
}
public function updated(string $propertyName, $value)
{
$this->canOrAbort('manage stock opname');
$this->form->update();
if (str_starts_with($propertyName, 'form.outlet_stock.')) {
$itemId = explode('.', $propertyName)[2];
$item = $this->items->firstWhere('id', $itemId);
if ($item) {
$item->qty_physical = $value;
$item->difference = $value - $item->qty_system;
}
}
}
public function render()
{
return view('livewire.studio.manage.stock-opname.manage', [
'pageTitle' => 'Kelola Stock Opname',
]);
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace App\Livewire\Studio\Manage\StockOpname;
use App\Models\StockOpname;
use App\Models\StockOpnameItem;
use App\Traits\WithAuthorization;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Title('Lihat Stock Opname')]
class Show extends Component
{
use WithAuthorization;
public $items;
public StockOpname $stockOpname;
public function mount(StockOpname $stockOpname)
{
$this->items = StockOpnameItem::with('itemable')
->where('stock_opname_id', $stockOpname->id)
->get();
$this->stockOpname = $stockOpname;
}
public function render()
{
return view('livewire.studio.manage.stock-opname.show', [
'pageTitle' => 'Lihat Stock Opname',
]);
}
}

View File

@ -4,6 +4,8 @@
use App\Enums\OutletStatus;
use Dyrynda\Database\Support\CascadeSoftDeletes;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
@ -37,6 +39,12 @@ public function getSlugOptions(): SlugOptions
->saveSlugsTo('slug');
}
#[Scope]
public function operational(Builder $query): void
{
$query->where('status', OutletStatus::OPERATIONAL->value);
}
public function openingHours(): HasMany
{
return $this->hasMany(OpeningHour::class);
@ -54,17 +62,17 @@ public function vouchers(): BelongsToMany
public function perfumes(): BelongsToMany
{
return $this->belongsToMany(Perfume::class);
return $this->belongsToMany(Perfume::class)->withPivot('stock');
}
public function products(): BelongsToMany
{
return $this->belongsToMany(Product::class);
return $this->belongsToMany(Product::class)->withPivot('stock');
}
public function bottles(): BelongsToMany
{
return $this->belongsToMany(Bottle::class);
return $this->belongsToMany(Bottle::class)->withPivot('stock');
}
public function expenses(): HasMany
@ -86,4 +94,9 @@ public function orders(): HasMany
{
return $this->hasMany(Order::class);
}
public function stockOpname(): HasMany
{
return $this->hasMany(StockOpname::class);
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace App\Models;
use App\Enums\StockOpnameStatus;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Veelasky\LaravelHashId\Eloquent\HashableId;
class StockOpname extends Model
{
use HashableId, SoftDeletes;
protected $table = 'stock_opname';
protected $guarded = ['id'];
protected $cascadeDeletes = ['items'];
protected function casts(): array
{
return [
'status' => StockOpnameStatus::class,
];
}
public function outlet(): BelongsTo
{
return $this->belongsTo(Outlet::class);
}
public function items(): HasMany
{
return $this->hasMany(StockOpnameItem::class);
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Database\Eloquent\SoftDeletes;
use Veelasky\LaravelHashId\Eloquent\HashableId;
class StockOpnameItem extends Model
{
use HashableId, SoftDeletes;
protected $guarded = ['id'];
protected function casts(): array
{
return [
'qty_system' => 'int',
'qty_physical' => 'int',
'differences' => 'int',
];
}
public function stockOpname(): BelongsTo
{
return $this->belongsTo(StockOpname::class);
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function itemable(): MorphTo
{
return $this->morphTo();
}
}

View File

@ -4,6 +4,7 @@
use App\Enums\PriceRequestStatus;
use App\Models\PriceRequest;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;
@ -11,6 +12,10 @@ class ViewServiceProvider extends ServiceProvider
{
public function boot(): void
{
if (! Schema::hasTable('price_requests')) {
return;
}
$priceRequestCount = PriceRequest::where('status', PriceRequestStatus::PENDING->value)->count();
View::composer('*', function ($view) use ($priceRequestCount) {
@ -162,6 +167,13 @@ public function boot(): void
'match' => 'studio.manage.purchase.*',
'can' => 'view purchase',
],
[
'label' => 'Stock Opname',
'icon' => 'clipboard-document-check',
'route' => 'studio.manage.stock_opname.index',
'match' => 'studio.manage.stock_opname.*',
'can' => 'view stock opname',
],
[
'label' => 'Artikel',
'icon' => 'newspaper',

View File

@ -0,0 +1,34 @@
<?php
use App\Enums\StockOpnameStatus;
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::create('stock_opname', function (Blueprint $table) {
$table->id();
$table->foreignId('outlet_id')->constrained()->cascadeOnDelete();
$table->string('period_month', 7);
$table->enum('status', [StockOpnameStatus::values()])->default(StockOpnameStatus::DRAFT)->comment(StockOpnameStatus::comment());
$table->text('note')->nullable();
$table->timestamp('created_at')->useCurrent();
$table->timestamp('updated_at')->nullable()->useCurrentOnUpdate();
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('stock_opname');
}
};

View File

@ -0,0 +1,35 @@
<?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::create('stock_opname_items', function (Blueprint $table) {
$table->id();
$table->foreignId('stock_opname_id')->constrained('stock_opname')->cascadeOnDelete();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->morphs('itemable');
$table->unsignedInteger('qty_system');
$table->unsignedInteger('qty_physical')->nullable();
$table->integer('difference')->storedAs('CAST(qty_physical AS SIGNED) - CAST(qty_system AS SIGNED)');
$table->timestamp('created_at')->useCurrent();
$table->timestamp('updated_at')->nullable()->useCurrentOnUpdate();
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('stock_opname_items');
}
};

View File

@ -113,6 +113,11 @@ public function run(): void
'create purchase',
'delete purchase',
'view stock opname',
'manage stock opname',
'show stock opname',
'approve stock opname',
'view article',
'create article',
'update article',
@ -242,6 +247,8 @@ public function run(): void
'create purchase',
'delete purchase',
'approve stock opname',
'view faq',
'create faq',
'update faq',
@ -346,6 +353,10 @@ public function run(): void
'create purchase',
'delete purchase',
'view stock opname',
'manage stock opname',
'show stock opname',
'view article',
'create article',
'update article',

View File

@ -0,0 +1,5 @@
<flux:tooltip content="{{ $label }}">
<flux:button href="{{ $route }}" variant="primary" color="{{ $buttonColor }}" icon="{{ $buttonIcon }}"
size="sm" wire:navigate>
</flux:button>
</flux:tooltip>

View File

@ -0,0 +1,106 @@
<flux:main>
<div class="flex justify-between items-center">
<div>
<flux:heading size="xl">{{ $pageTitle }}</flux:heading>
</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>
@foreach ($stockOpname as $item)
<flux:card class="h-auto" wire:key="stock-opname-{{ $item->id }}">
<div class="flex justify-between items-center mb-3">
<div>
<flux:heading>{{ $item->outlet->name }}</flux:heading>
<flux:text>{{ $item->period_month }}</flux:text>
</div>
<div class="text-right">
<flux:badge color="{{ $item->status->color() }}">
{{ $item->status->label() }}
</flux:badge>
</div>
</div>
<div class="grid grid-cols-1 gap-4 mb-3 text-sm">
<div>
<flux:text>Total Item</flux:text>
<div class="font-medium">{{ $item->total_item }}</div>
</div>
<div>
<flux:text>Progress</flux:text>
<div class="font-medium">
{{ $item->progress_filled }} / {{ $item->total_item }} |
({{ $item->progress_percent }}%)
</div>
</div>
@if ($item['note'])
<div>
<flux:text>Catatan</flux:text>
<div class="font-medium">
{{ $item['note'] }}
</div>
</div>
@endif
</div>
<flux:separator class="my-2" />
<div class="flex items-center mt-2 gap-2">
<flux:tooltip content="Lihat">
<flux:button variant="primary" color="blue" size="sm" icon="eye"
href="{{ route('studio.manage.stock_opname.show', ['stockOpname' => $item->hash]) }}"
wire:navigate>
</flux:button>
</flux:tooltip>
@if ($item->status === \App\Enums\StockOpnameStatus::PROCESS)
<flux:tooltip content="Kelola">
<flux:button variant="primary" color="yellow" size="sm" icon="document-text"
href="{{ route('studio.manage.stock_opname.manage', ['stockOpname' => $item->hash]) }}"
wire:navigate>
</flux:button>
</flux:tooltip>
@if ($item->progress_percent == 100)
<flux:modal.trigger name="confirmation-modal">
<flux:tooltip content="Ajukan Persetujuan">
<flux:button variant="primary" color="emerald" icon="document-check"
size="sm"
wire:click="$dispatch('fn:confirmAction', {
id : '{{ $item->hash }}',
message: 'Pastikan proses stock opname sudah tepat dan sesuai.',
confirmButtonText: 'Ajukan',
target: 'requestApproval',
confirmButtonColor : 'emerald'
})">
</flux:button>
</flux:tooltip>
</flux:modal.trigger>
@endif
@endif
@if ($item->status === \App\Enums\StockOpnameStatus::PENDING_APPROVAL && auth()->user()->can('approve stock opname'))
<flux:modal.trigger name="confirmation-modal">
<flux:tooltip content="Setujui">
<flux:button variant="primary" color="emerald" icon="check-circle" size="sm"
wire:click="$dispatch('fn:confirmAction', {
id : '{{ $item->hash }}',
message: 'Proses ini akan menyesuaikan kembali stok barang dengan hasil inputan dan proes tidak dapat dibatalkan, silakan periksa kembali dengan benar.',
confirmButtonText: 'Ajukan',
target: 'approveApproval',
confirmButtonColor : 'emerald'
})">
</flux:button>
</flux:tooltip>
</flux:modal.trigger>
@endif
</div>
</flux:card>
@endforeach
</div>
<livewire:datatable.studio.manage.stock-opname-table />
</div>
@include('components.confirmation')
</flux:main>

View File

@ -0,0 +1,48 @@
<flux:main>
<div class="flex justify-between items-center">
<div>
<flux:heading size="xl">{{ $pageTitle }}</flux:heading>
</div>
<div>
<flux:button href="{{ route('studio.manage.stock_opname.index') }}" wire:navigate.hover class="text-sm">
Kembali
</flux:button>
</div>
</div>
<div class="mt-6">
<div class="space-y-4">
<flux:card>
<flux:table>
<flux:table.columns>
<flux:table.column>Item</flux:table.column>
<flux:table.column>Stok Sistem</flux:table.column>
<flux:table.column>Stok Outlet</flux:table.column>
<flux:table.column>Selisih</flux:table.column>
</flux:table.columns>
<flux:table.rows>
@foreach ($items as $item)
<flux:table.row wire:key="{{ $item->id }}">
<flux:table.cell>{{ $item->itemable?->name }}</flux:table.cell>
<flux:table.cell>{{ $item->qty_system }}</flux:table.cell>
<flux:table.cell>
<flux:input wire:model.blur="form.outlet_stock.{{ $item->id }}"
x-mask:dynamic="$money($input, ',')" autocomplete="off" />
</flux:table.cell>
<flux:table.cell variant="strong">
{{ $item->difference }}
</flux:table.cell>
</flux:table.row>
@endforeach
</flux:table.rows>
</flux:table>
</flux:card>
<flux:card>
<flux:textarea label="Catatan" placeholder="Blue emotion tumpah 10ml" wire:model.blur="form.note"
autocomplete="off" rows="2" />
</flux:card>
</div>
</div>
</flux:main>

View File

@ -0,0 +1,54 @@
<flux:main>
<div class="flex justify-between items-center">
<div>
<flux:heading size="xl">{{ $pageTitle }}</flux:heading>
</div>
<div>
<flux:button href="{{ route('studio.manage.stock_opname.index') }}" wire:navigate.hover class="text-sm">
Kembali
</flux:button>
</div>
</div>
<div class="mt-6">
<div class="space-y-4">
<flux:card>
<flux:table>
<flux:table.columns>
<flux:table.column>Item</flux:table.column>
<flux:table.column>Stok Sistem</flux:table.column>
<flux:table.column>Stok Outlet</flux:table.column>
<flux:table.column>Selisih</flux:table.column>
</flux:table.columns>
<flux:table.rows>
@foreach ($items as $item)
<flux:table.row wire:key="{{ $item->id }}">
<flux:table.cell>{{ $item->itemable?->name }}</flux:table.cell>
<flux:table.cell>{{ $item->qty_system }}</flux:table.cell>
<flux:table.cell>{{ $item->qty_physical }}</flux:table.cell>
<flux:table.cell variant="strong">
{{ $item->difference }}
</flux:table.cell>
</flux:table.row>
@endforeach
</flux:table.rows>
</flux:table>
</flux:card>
@if ($stockOpname['note'])
<flux:card>
<div>
<flux:heading size="lg">Catatan</flux:heading>
</div>
<div class="mt-6">
<flux:text>
{{ $stockOpname['note'] }}
</flux:text>
</div>
</flux:card>
@endif
</div>
</div>
</flux:main>

View File

@ -36,6 +36,9 @@
use App\Livewire\Studio\Manage\Order\Show as OrderShow;
use App\Livewire\Studio\Manage\Purchase\Create as PurchaseCreate;
use App\Livewire\Studio\Manage\Purchase\Index as PurchaseIndex;
use App\Livewire\Studio\Manage\StockOpname\Index as StockOpnameIndex;
use App\Livewire\Studio\Manage\StockOpname\Manage as StockOpnameManage;
use App\Livewire\Studio\Manage\StockOpname\Show as StockOpnameShow;
use App\Livewire\Studio\Master\Formula as FormulaComponent;
use App\Livewire\Studio\Master\Outlet\Create as OutletCreate;
use App\Livewire\Studio\Master\Outlet\Edit as OutletEdit;
@ -162,6 +165,12 @@
Route::delete('/{purchase}/delete', PurchaseCreate::class)->name('delete')->middleware('can:delete purchase');
});
Route::prefix('stock-opname')->name('stock_opname.')->group(function () {
Route::get('/', StockOpnameIndex::class)->name('index')->middleware('can:view stock opname');
Route::get('/{stockOpname}/show', StockOpnameShow::class)->name('show')->middleware('can:show stock opname');
Route::get('/{stockOpname}/manage', StockOpnameManage::class)->name('manage')->middleware('can:manage stock opname');
});
Route::prefix('articles')->name('article.')->group(function () {
Route::get('/', ArticleIndex::class)->name('index')->middleware('can:view article');
Route::get('/create', ArticleCreate::class)->name('create')->middleware('can:create article');