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']]); $route = route('studio.catalog.bottle.audit', [$value['id'], $value['bottle_id']]);
$canAudit = auth()->user()->can('audit bottle'); $canAudit = auth()->user()->can('audit bottle');
$tag = $canAudit ? 'div' : 'a'; $tag = ! $canAudit ? 'div' : 'a';
$attrs = $canAudit ? '' : 'href="'.$route.'" wire:navigate'; $attrs = ! $canAudit ? '' : 'href="'.$route.'" wire:navigate';
return Blade::render(" return Blade::render("
<{$tag} {$attrs} <{$tag} {$attrs}

View File

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

View File

@ -87,8 +87,8 @@ public function columns(): array
$route = route('studio.catalog.product.audit', [$value['id'], $value['product_id']]); $route = route('studio.catalog.product.audit', [$value['id'], $value['product_id']]);
$canAudit = auth()->user()->can('audit product'); $canAudit = auth()->user()->can('audit product');
$tag = $canAudit ? 'div' : 'a'; $tag = ! $canAudit ? 'div' : 'a';
$attrs = $canAudit ? '' : 'href="'.$route.'" wire:navigate'; $attrs = ! $canAudit ? '' : 'href="'.$route.'" wire:navigate';
return Blade::render(" return Blade::render("
<{$tag} {$attrs} <{$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 App\Enums\OutletStatus;
use Dyrynda\Database\Support\CascadeSoftDeletes; 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\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\BelongsToMany;
@ -37,6 +39,12 @@ public function getSlugOptions(): SlugOptions
->saveSlugsTo('slug'); ->saveSlugsTo('slug');
} }
#[Scope]
public function operational(Builder $query): void
{
$query->where('status', OutletStatus::OPERATIONAL->value);
}
public function openingHours(): HasMany public function openingHours(): HasMany
{ {
return $this->hasMany(OpeningHour::class); return $this->hasMany(OpeningHour::class);
@ -54,17 +62,17 @@ public function vouchers(): BelongsToMany
public function perfumes(): BelongsToMany public function perfumes(): BelongsToMany
{ {
return $this->belongsToMany(Perfume::class); return $this->belongsToMany(Perfume::class)->withPivot('stock');
} }
public function products(): BelongsToMany public function products(): BelongsToMany
{ {
return $this->belongsToMany(Product::class); return $this->belongsToMany(Product::class)->withPivot('stock');
} }
public function bottles(): BelongsToMany public function bottles(): BelongsToMany
{ {
return $this->belongsToMany(Bottle::class); return $this->belongsToMany(Bottle::class)->withPivot('stock');
} }
public function expenses(): HasMany public function expenses(): HasMany
@ -86,4 +94,9 @@ public function orders(): HasMany
{ {
return $this->hasMany(Order::class); 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\Enums\PriceRequestStatus;
use App\Models\PriceRequest; use App\Models\PriceRequest;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\View; use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
@ -11,6 +12,10 @@ class ViewServiceProvider extends ServiceProvider
{ {
public function boot(): void public function boot(): void
{ {
if (! Schema::hasTable('price_requests')) {
return;
}
$priceRequestCount = PriceRequest::where('status', PriceRequestStatus::PENDING->value)->count(); $priceRequestCount = PriceRequest::where('status', PriceRequestStatus::PENDING->value)->count();
View::composer('*', function ($view) use ($priceRequestCount) { View::composer('*', function ($view) use ($priceRequestCount) {
@ -162,6 +167,13 @@ public function boot(): void
'match' => 'studio.manage.purchase.*', 'match' => 'studio.manage.purchase.*',
'can' => 'view 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', 'label' => 'Artikel',
'icon' => 'newspaper', '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', 'create purchase',
'delete purchase', 'delete purchase',
'view stock opname',
'manage stock opname',
'show stock opname',
'approve stock opname',
'view article', 'view article',
'create article', 'create article',
'update article', 'update article',
@ -242,6 +247,8 @@ public function run(): void
'create purchase', 'create purchase',
'delete purchase', 'delete purchase',
'approve stock opname',
'view faq', 'view faq',
'create faq', 'create faq',
'update faq', 'update faq',
@ -346,6 +353,10 @@ public function run(): void
'create purchase', 'create purchase',
'delete purchase', 'delete purchase',
'view stock opname',
'manage stock opname',
'show stock opname',
'view article', 'view article',
'create article', 'create article',
'update 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\Order\Show as OrderShow;
use App\Livewire\Studio\Manage\Purchase\Create as PurchaseCreate; use App\Livewire\Studio\Manage\Purchase\Create as PurchaseCreate;
use App\Livewire\Studio\Manage\Purchase\Index as PurchaseIndex; 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\Formula as FormulaComponent;
use App\Livewire\Studio\Master\Outlet\Create as OutletCreate; use App\Livewire\Studio\Master\Outlet\Create as OutletCreate;
use App\Livewire\Studio\Master\Outlet\Edit as OutletEdit; 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::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::prefix('articles')->name('article.')->group(function () {
Route::get('/', ArticleIndex::class)->name('index')->middleware('can:view article'); Route::get('/', ArticleIndex::class)->name('index')->middleware('can:view article');
Route::get('/create', ArticleCreate::class)->name('create')->middleware('can:create article'); Route::get('/create', ArticleCreate::class)->name('create')->middleware('can:create article');