feat: add stock transfer management functionality
- Implemented stock transfer creation and management with Livewire components. - Created StockTransfer and StockTransferItem models with necessary relationships. - Added migration files for stock_transfers and stock_transfer_items tables. - Developed traits for handling stock transfer items (add, delete, update). - Integrated stock transfer logging in StockActivityLogService. - Updated ViewServiceProvider to include stock transfer permissions and routes. - Created frontend views for stock transfer form and index with appropriate UI components. - Added role permissions for stock transfer actions in RolePermissionSeeder.
This commit is contained in:
parent
156eca433c
commit
dffc9f9e72
78
app/Livewire/Datatable/Manage/StockTransfersTable.php
Normal file
78
app/Livewire/Datatable/Manage/StockTransfersTable.php
Normal file
@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Datatable\Manage;
|
||||
|
||||
use App\Models\StockTransfer;
|
||||
use App\Traits\Datatable\WithAppendColumn;
|
||||
use App\Traits\Datatable\WithConfiguration;
|
||||
use App\Traits\Datatable\WithPrependColumn;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Rappasoft\LaravelLivewireTables\DataTableComponent;
|
||||
use Rappasoft\LaravelLivewireTables\Views\Column;
|
||||
use Rappasoft\LaravelLivewireTables\Views\Columns\ArrayColumn;
|
||||
|
||||
class StockTransfersTable extends DataTableComponent
|
||||
{
|
||||
use WithAppendColumn, WithConfiguration, WithPrependColumn;
|
||||
|
||||
protected $model = StockTransfer::class;
|
||||
|
||||
protected string $sortColumn = 'transfer_date';
|
||||
|
||||
protected string $sortDirection = 'desc';
|
||||
|
||||
public function columns(): array
|
||||
{
|
||||
return [
|
||||
Column::make('Outlet Asal', 'sourceOutlet.name')->searchable(),
|
||||
|
||||
Column::make('Outlet Tujuan', 'destinationOutlet.name')->searchable(),
|
||||
|
||||
Column::make('Tanggal Transfer', 'transfer_date')
|
||||
->format(fn ($value) => formatDateLocalized($value))
|
||||
->searchable()
|
||||
->sortable(),
|
||||
|
||||
Column::make('Catatan', 'note')->searchable(),
|
||||
|
||||
ArrayColumn::make('Item')
|
||||
->data(
|
||||
fn ($value, $row) => $row->items->map(fn ($item) => [
|
||||
'name' => $item->transferable?->name,
|
||||
'quantity' => formatCurrencyNumber($item->quantity, ''),
|
||||
])->toArray()
|
||||
)
|
||||
->outputFormat(
|
||||
fn ($index, $value) => "
|
||||
<div class='text-[13px] leading-tight mb-1'>
|
||||
<div class='font-bold text-gray-800 dark:text-white'>{$value['name']}</div>
|
||||
<div class='text-gray-400 text-[12px]'>Qty: {$value['quantity']}</div>
|
||||
</div>"
|
||||
)
|
||||
->flexCol(['class' => 'flex-col gap-3']),
|
||||
|
||||
Column::make('Aksi')
|
||||
->label(function ($row) {
|
||||
$actions = '';
|
||||
|
||||
if (auth()->user()->can('delete stock transfer')) {
|
||||
$actions .= view('components.actions.table.delete', [
|
||||
'id' => $row->hash,
|
||||
])->render();
|
||||
}
|
||||
|
||||
return $actions;
|
||||
})
|
||||
->html()
|
||||
->hideIf(auth()->user()->cannot('update stock transfer')),
|
||||
];
|
||||
}
|
||||
|
||||
public function builder(): Builder
|
||||
{
|
||||
return StockTransfer::select('stock_transfers.id', 'source_outlet_id', 'destination_outlet_id', 'transfer_date', 'note')
|
||||
->with(['sourceOutlet', 'destinationOutlet', 'items', 'items.transferable'])
|
||||
->whereIn('source_outlet_id', auth()->user()->outlets->pluck('id')->toArray())
|
||||
->orWhereIn('destination_outlet_id', auth()->user()->outlets->pluck('id')->toArray());
|
||||
}
|
||||
}
|
||||
129
app/Livewire/Forms/Studio/Manage/StockTransferForm.php
Normal file
129
app/Livewire/Forms/Studio/Manage/StockTransferForm.php
Normal file
@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Forms\Studio\Manage;
|
||||
|
||||
use App\Models\StockTransfer;
|
||||
use App\Traits\Media\WithMediaHandler;
|
||||
use App\Traits\StockTransfer\WithStockTransferItems;
|
||||
use App\Traits\StockTransfer\WithStockTransferStock;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Livewire\Form;
|
||||
|
||||
class StockTransferForm extends Form
|
||||
{
|
||||
use WithMediaHandler, WithStockTransferItems, WithStockTransferStock;
|
||||
|
||||
public ?StockTransfer $stockTransfer = null;
|
||||
|
||||
public ?string $note = null;
|
||||
|
||||
public string $transfer_date = '';
|
||||
|
||||
public string $source_outlet_id = '';
|
||||
|
||||
public string $destination_outlet_id = '';
|
||||
|
||||
public array $image = [];
|
||||
|
||||
public string $perfume_id = '';
|
||||
|
||||
public string $quantity_perfume = '';
|
||||
|
||||
public string $product_id = '';
|
||||
|
||||
public string $quantity_product = '';
|
||||
|
||||
public string $bottle_id = '';
|
||||
|
||||
public string $quantity_bottle = '';
|
||||
|
||||
public string $item_id = '';
|
||||
|
||||
public string $quantity_edit = '';
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'note' => ['nullable', 'string', 'max:100'],
|
||||
'transfer_date' => ['required', 'date', 'before_or_equal:today'],
|
||||
'source_outlet_id' => ['required', Rule::exists('outlets', 'id')],
|
||||
'destination_outlet_id' => ['required', Rule::exists('outlets', 'id'), 'different:source_outlet_id'],
|
||||
'image' => ['nullable', 'array', 'max:1'],
|
||||
];
|
||||
}
|
||||
|
||||
public function validationAttributes(): array
|
||||
{
|
||||
return [
|
||||
'note' => 'catatan',
|
||||
'transfer_date' => 'tanggal transfer',
|
||||
'source_outlet_id' => 'outlet asal',
|
||||
'destination_outlet_id' => 'outlet tujuan',
|
||||
'image' => 'gambar',
|
||||
];
|
||||
}
|
||||
|
||||
public function setStockTransfer(StockTransfer $stockTransfer): void
|
||||
{
|
||||
$this->stockTransfer = $stockTransfer;
|
||||
|
||||
$this->note = $stockTransfer->note;
|
||||
$this->transfer_date = $stockTransfer->transfer_date;
|
||||
$this->source_outlet_id = $stockTransfer->source_outlet_id;
|
||||
$this->destination_outlet_id = $stockTransfer->destination_outlet_id;
|
||||
|
||||
$this->image = $this->mapMediaCollection($stockTransfer->getMedia('image'));
|
||||
}
|
||||
|
||||
public function store(): void
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
$stockTransferItems = $this->loadStockTransferItems();
|
||||
|
||||
DB::transaction(function () use ($stockTransferItems) {
|
||||
$data = $this->prepareSavedData();
|
||||
|
||||
$stockTransfer = StockTransfer::create($data);
|
||||
|
||||
$stockTransfer->load(['sourceOutlet', 'destinationOutlet']);
|
||||
|
||||
$this->uploadMedia($this->image, $stockTransfer, 'image');
|
||||
|
||||
foreach ($stockTransferItems as $item) {
|
||||
if ($item->quantity <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$item->update(['stock_transfer_id' => $stockTransfer->id]);
|
||||
|
||||
$this->transferStockBetweenOutlets($stockTransfer->sourceOutlet, $stockTransfer->destinationOutlet, $item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function update(): void
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
DB::transaction(function () {
|
||||
$data = $this->prepareSavedData();
|
||||
|
||||
$this->stockTransfer->update($data);
|
||||
|
||||
$this->syncMedia($this->image, $this->stockTransfer, 'image');
|
||||
$this->uploadMedia($this->image, $this->stockTransfer, 'image');
|
||||
});
|
||||
}
|
||||
|
||||
private function prepareSavedData(): array
|
||||
{
|
||||
return [
|
||||
'note' => $this->note,
|
||||
'source_outlet_id' => $this->source_outlet_id,
|
||||
'destination_outlet_id' => $this->destination_outlet_id,
|
||||
'transfer_date' => $this->transfer_date,
|
||||
];
|
||||
}
|
||||
}
|
||||
130
app/Livewire/Studio/Manage/StockTransfer/Create.php
Normal file
130
app/Livewire/Studio/Manage/StockTransfer/Create.php
Normal file
@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Studio\Manage\StockTransfer;
|
||||
|
||||
use App\Livewire\Forms\Studio\Manage\StockTransferForm;
|
||||
use App\Models\Outlet;
|
||||
use App\Traits\Authorization\WithAuthorization;
|
||||
use App\Traits\Components\WithConfirmation;
|
||||
use App\Traits\Components\WithToast;
|
||||
use App\Traits\StockTransfer\WithStockTransferAddItem;
|
||||
use App\Traits\StockTransfer\WithStockTransferDeleteItem;
|
||||
use App\Traits\StockTransfer\WithStockTransferItems;
|
||||
use App\Traits\StockTransfer\WithStockTransferUpdateItem;
|
||||
use App\Traits\Utilities\WithUpdatedData;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Livewire\Attributes\Title;
|
||||
use Livewire\Component;
|
||||
|
||||
#[Title('Tambah Transfer Stok Antar Outlet')]
|
||||
class Create extends Component
|
||||
{
|
||||
use WithAuthorization, WithConfirmation, WithStockTransferAddItem, WithStockTransferDeleteItem, WithStockTransferItems, WithStockTransferUpdateItem, WithToast, WithUpdatedData;
|
||||
|
||||
public StockTransferForm $form;
|
||||
|
||||
public array $sourceOutlets = [];
|
||||
|
||||
public array $destinationOutlets = [];
|
||||
|
||||
public array $perfumes = [];
|
||||
|
||||
public array $products = [];
|
||||
|
||||
public array $bottles = [];
|
||||
|
||||
public $stockTransferItems;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->sourceOutlets = auth()->user()->outlets->pluck('name', 'id')->toArray();
|
||||
$this->destinationOutlets = Outlet::orderBy('name')->pluck('name', 'id')->toArray();
|
||||
|
||||
$this->stockTransferItems = $this->loadStockTransferItems();
|
||||
}
|
||||
|
||||
public function updated($property, $value): void
|
||||
{
|
||||
if ($property === 'form.source_outlet_id' || $property === 'form.destination_outlet_id') {
|
||||
$this->loadCreateItems();
|
||||
}
|
||||
}
|
||||
|
||||
private function loadCreateItems(): void
|
||||
{
|
||||
$this->reset(['perfumes', 'products', 'bottles']);
|
||||
|
||||
$this->form->reset(
|
||||
'perfume_id',
|
||||
'quantity_perfume',
|
||||
'product_id',
|
||||
'quantity_product',
|
||||
'bottle_id',
|
||||
'quantity_bottle'
|
||||
);
|
||||
|
||||
if (! $this->form->source_outlet_id || ! $this->form->destination_outlet_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
$sourceOutlet = Outlet::find($this->form->source_outlet_id);
|
||||
|
||||
if (! $sourceOutlet) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->perfumes = $sourceOutlet->perfumes()
|
||||
->wherePivot('stock', '>', 0)
|
||||
->orderBy('name')
|
||||
->get()
|
||||
->pluck('name', 'id')
|
||||
->toArray();
|
||||
|
||||
$this->products = $sourceOutlet->products()
|
||||
->wherePivot('stock', '>', 0)
|
||||
->orderBy('name')
|
||||
->get()
|
||||
->pluck('name', 'id')
|
||||
->toArray();
|
||||
|
||||
$this->bottles = $sourceOutlet->bottles()
|
||||
->wherePivot('stock', '>', 0)
|
||||
->orderBy('name')
|
||||
->get()
|
||||
->pluck('name', 'id')
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
$this->canOrAbort('create stock transfer');
|
||||
|
||||
if ($this->stockTransferItems->isEmpty()) {
|
||||
$this->toast('Keranjang transfer stok tidak boleh kosong.', 'Gagal', 'danger');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->form->store();
|
||||
} catch (ValidationException $e) {
|
||||
throw $e;
|
||||
} catch (\Exception $e) {
|
||||
$this->toast($e->getMessage(), 'Gagal', 'danger');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->redirectRoute('studio.manage.stock_transfer.index', [
|
||||
'notification' => 'Transfer stok berhasil diproses.',
|
||||
], navigate: true);
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
{
|
||||
return view('livewire.studio.manage.stock-transfer.form', [
|
||||
'pageTitle' => 'Tambah Transfer Stok Antar Outlet',
|
||||
]);
|
||||
}
|
||||
}
|
||||
49
app/Livewire/Studio/Manage/StockTransfer/Index.php
Normal file
49
app/Livewire/Studio/Manage/StockTransfer/Index.php
Normal file
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Studio\Manage\StockTransfer;
|
||||
|
||||
use App\Models\StockTransfer;
|
||||
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\StockTransfer\WithStockTransferStock;
|
||||
use App\Traits\Utilities\WithRequestNotification;
|
||||
use Flux\Flux;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Livewire\Attributes\Title;
|
||||
use Livewire\Component;
|
||||
|
||||
#[Title('Transfer Stok')]
|
||||
class Index extends Component
|
||||
{
|
||||
use WithAuthorization, WithCloseModal, WithConfirmation, WithRequestNotification, WithStockTransferStock, WithSubscribeNotification, WithToast;
|
||||
|
||||
public function delete(StockTransfer $stockTransfer): void
|
||||
{
|
||||
$this->canOrAbort('delete stock transfer');
|
||||
|
||||
DB::transaction(function () use ($stockTransfer) {
|
||||
foreach ($stockTransfer->items as $item) {
|
||||
$this->reverseStockTransferBetweenOutlets($stockTransfer->sourceOutlet, $stockTransfer->destinationOutlet, $item);
|
||||
}
|
||||
|
||||
$stockTransfer->delete();
|
||||
});
|
||||
|
||||
$this->dispatch('refreshDatatable');
|
||||
|
||||
$this->toast('Transfer stok berhasil dihapus.');
|
||||
|
||||
Flux::modals()->close();
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
{
|
||||
return view('livewire.studio.manage.stock-transfer.index', [
|
||||
'pageTitle' => 'Transfer Stok Antar Outlet',
|
||||
]);
|
||||
}
|
||||
}
|
||||
55
app/Models/StockTransfer.php
Normal file
55
app/Models/StockTransfer.php
Normal file
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
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\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Spatie\MediaLibrary\HasMedia;
|
||||
use Spatie\MediaLibrary\InteractsWithMedia;
|
||||
use Veelasky\LaravelHashId\Eloquent\HashableId;
|
||||
|
||||
class StockTransfer extends Model implements HasMedia
|
||||
{
|
||||
use HasFactory, HashableId, InteractsWithMedia, SoftDeletes;
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'transfer_date' => 'date',
|
||||
];
|
||||
}
|
||||
|
||||
#[Scope]
|
||||
public function today(Builder $query): void
|
||||
{
|
||||
$query->whereDate('created_at', today());
|
||||
}
|
||||
|
||||
#[Scope]
|
||||
public function yesterday(Builder $query): void
|
||||
{
|
||||
$query->whereDate('created_at', today()->subDay());
|
||||
}
|
||||
|
||||
public function sourceOutlet(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Outlet::class, 'source_outlet_id');
|
||||
}
|
||||
|
||||
public function destinationOutlet(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Outlet::class, 'destination_outlet_id');
|
||||
}
|
||||
|
||||
public function items(): HasMany
|
||||
{
|
||||
return $this->hasMany(StockTransferItem::class);
|
||||
}
|
||||
}
|
||||
46
app/Models/StockTransferItem.php
Normal file
46
app/Models/StockTransferItem.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Scope;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
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 StockTransferItem extends Model
|
||||
{
|
||||
use HashableId, SoftDeletes;
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'quantity' => 'int',
|
||||
];
|
||||
}
|
||||
|
||||
#[Scope]
|
||||
public function currentUser(Builder $query): void
|
||||
{
|
||||
$query->where('user_id', auth()->id());
|
||||
}
|
||||
|
||||
public function stockTransfer(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(StockTransfer::class);
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function transferable(): MorphTo
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
}
|
||||
@ -188,6 +188,13 @@ public function boot(): void
|
||||
'match' => 'studio.manage.restock.*',
|
||||
'can' => 'view restock',
|
||||
],
|
||||
[
|
||||
'label' => 'Transfer Stok',
|
||||
'icon' => 'clipboard-document-list',
|
||||
'route' => 'studio.manage.stock_transfer.index',
|
||||
'match' => 'studio.manage.stock_transfer.*',
|
||||
'can' => 'view stock transfer',
|
||||
],
|
||||
[
|
||||
'label' => 'Stock Opname',
|
||||
'icon' => 'clipboard-document-check',
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
use App\Models\Purchase;
|
||||
use App\Models\Restock;
|
||||
use App\Models\StockOpname;
|
||||
use App\Models\StockTransfer;
|
||||
use App\Models\Warehouse;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
@ -20,6 +21,8 @@ class StockActivityLogService
|
||||
|
||||
public const LOG_STOCK_OPNAME = 'stock_opname';
|
||||
|
||||
public const LOG_TRANSFER = 'transfer';
|
||||
|
||||
/**
|
||||
* Log purchase activity (Incoming to Warehouse).
|
||||
*/
|
||||
@ -159,6 +162,57 @@ public static function stockOpname(
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log transfer activity (Outlet to Outlet).
|
||||
* This logs two events: Out from Source Outlet, In to Destination Outlet.
|
||||
*/
|
||||
public static function transfer(
|
||||
StockTransfer $stockTransfer,
|
||||
Model $product,
|
||||
int $quantity,
|
||||
int $sourcePreviousStock,
|
||||
int $sourceNewStock,
|
||||
int $destinationPreviousStock,
|
||||
int $destinationNewStock,
|
||||
?Model $causedBy = null
|
||||
): void {
|
||||
// Log Source Outlet Out
|
||||
self::logOutlet(
|
||||
logName: self::LOG_TRANSFER,
|
||||
event: 'decrease',
|
||||
description: "Stock Transfer #{$stockTransfer->hash_id}: Moved {$quantity} stock from Source to Destination Outlet.",
|
||||
performedOn: $product,
|
||||
outlet: $stockTransfer->sourceOutlet,
|
||||
quantityChange: -1 * abs($quantity),
|
||||
previousStock: $sourcePreviousStock,
|
||||
newStock: $sourceNewStock,
|
||||
extraProperties: [
|
||||
'stock_transfer_id' => $stockTransfer->id,
|
||||
'stock_transfer_hash' => $stockTransfer->hash_id,
|
||||
'destination_outlet_id' => $stockTransfer->destination_outlet_id,
|
||||
],
|
||||
causedBy: $causedBy
|
||||
);
|
||||
|
||||
// Log Destination Outlet In
|
||||
self::logOutlet(
|
||||
logName: self::LOG_TRANSFER,
|
||||
event: 'increase',
|
||||
description: "Stock Transfer #{$stockTransfer->hash_id}: Received {$quantity} stock from Source Outlet.",
|
||||
performedOn: $product,
|
||||
outlet: $stockTransfer->destinationOutlet,
|
||||
quantityChange: abs($quantity),
|
||||
previousStock: $destinationPreviousStock,
|
||||
newStock: $destinationNewStock,
|
||||
extraProperties: [
|
||||
'stock_transfer_id' => $stockTransfer->id,
|
||||
'stock_transfer_hash' => $stockTransfer->hash_id,
|
||||
'source_outlet_id' => $stockTransfer->source_outlet_id,
|
||||
],
|
||||
causedBy: $causedBy
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log stock related activity for Outlet.
|
||||
*/
|
||||
|
||||
112
app/Traits/StockTransfer/WithStockTransferAddItem.php
Normal file
112
app/Traits/StockTransfer/WithStockTransferAddItem.php
Normal file
@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace App\Traits\StockTransfer;
|
||||
|
||||
use App\Models\Bottle;
|
||||
use App\Models\Outlet;
|
||||
use App\Models\Perfume;
|
||||
use App\Models\Product;
|
||||
use App\Models\StockTransferItem;
|
||||
|
||||
trait WithStockTransferAddItem
|
||||
{
|
||||
public function addItem(string $type): void
|
||||
{
|
||||
$this->canOrAbort('create stock transfer');
|
||||
|
||||
if (! $this->form->source_outlet_id) {
|
||||
$this->toast('Silakan pilih outlet asal terlebih dahulu.', 'Gagal', 'warning');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$sourceOutlet = Outlet::find($this->form->source_outlet_id);
|
||||
|
||||
if (! $sourceOutlet) {
|
||||
$this->toast('Outlet asal tidak ditemukan.', 'Gagal', 'danger');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$data = match ($type) {
|
||||
'parfum' => [
|
||||
'transferable_type' => Perfume::class,
|
||||
'transferable_id' => $this->form->perfume_id,
|
||||
'quantity' => parseRupiahToInt($this->form->quantity_perfume),
|
||||
],
|
||||
'produk' => [
|
||||
'transferable_type' => Product::class,
|
||||
'transferable_id' => $this->form->product_id,
|
||||
'quantity' => parseRupiahToInt($this->form->quantity_product),
|
||||
],
|
||||
'botol' => [
|
||||
'transferable_type' => Bottle::class,
|
||||
'transferable_id' => $this->form->bottle_id,
|
||||
'quantity' => parseRupiahToInt($this->form->quantity_bottle),
|
||||
],
|
||||
default => null,
|
||||
};
|
||||
|
||||
if (! $data || ! $data['transferable_id'] || $data['quantity'] <= 0) {
|
||||
$this->toast('Data item tidak valid.', 'Gagal', 'danger');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate stock availability
|
||||
$relationName = match ($type) {
|
||||
'parfum' => 'perfumes',
|
||||
'produk' => 'products',
|
||||
'botol' => 'bottles',
|
||||
default => null,
|
||||
};
|
||||
|
||||
if ($relationName) {
|
||||
$sourceStock = $sourceOutlet->{$relationName}()
|
||||
->where($data['transferable_type']::make()->getTable().'.id', $data['transferable_id'])
|
||||
->first()
|
||||
?->pivot
|
||||
?->stock ?? 0;
|
||||
|
||||
// Is item already in cart?
|
||||
$existing = StockTransferItem::where([
|
||||
'user_id' => auth()->id(),
|
||||
'transferable_id' => $data['transferable_id'],
|
||||
'transferable_type' => $data['transferable_type'],
|
||||
])->whereNull('stock_transfer_id')->first();
|
||||
|
||||
$currentCartQty = $existing ? $existing->quantity : 0;
|
||||
$requestedQty = $data['quantity'];
|
||||
|
||||
if (($currentCartQty + $requestedQty) > $sourceStock) {
|
||||
$this->toast("Stok tidak mencukupi. Stok di outlet asal: {$sourceStock}. Sudah di keranjang: {$currentCartQty}.", 'Gagal', 'danger');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($existing) {
|
||||
$existing->increment('quantity', $requestedQty);
|
||||
} else {
|
||||
StockTransferItem::create(array_merge($data, ['user_id' => auth()->id()]));
|
||||
}
|
||||
}
|
||||
|
||||
$this->resetFormItem($type);
|
||||
$this->stockTransferItems = $this->loadStockTransferItems();
|
||||
$this->toast('Item berhasil ditambahkan ke keranjang.');
|
||||
}
|
||||
|
||||
private function resetFormItem(string $type): void
|
||||
{
|
||||
if ($type === 'parfum') {
|
||||
$this->form->perfume_id = '';
|
||||
$this->form->quantity_perfume = '';
|
||||
} elseif ($type === 'produk') {
|
||||
$this->form->product_id = '';
|
||||
$this->form->quantity_product = '';
|
||||
} elseif ($type === 'botol') {
|
||||
$this->form->bottle_id = '';
|
||||
$this->form->quantity_bottle = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
18
app/Traits/StockTransfer/WithStockTransferDeleteItem.php
Normal file
18
app/Traits/StockTransfer/WithStockTransferDeleteItem.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Traits\StockTransfer;
|
||||
|
||||
use App\Models\StockTransferItem;
|
||||
|
||||
trait WithStockTransferDeleteItem
|
||||
{
|
||||
public function deleteItem(StockTransferItem $item): void
|
||||
{
|
||||
$this->canOrAbort('create stock transfer');
|
||||
|
||||
$item->delete();
|
||||
|
||||
$this->stockTransferItems = $this->loadStockTransferItems();
|
||||
$this->toast('Item berhasil dihapus dari keranjang.');
|
||||
}
|
||||
}
|
||||
19
app/Traits/StockTransfer/WithStockTransferItems.php
Normal file
19
app/Traits/StockTransfer/WithStockTransferItems.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Traits\StockTransfer;
|
||||
|
||||
use App\Models\StockTransferItem;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
|
||||
trait WithStockTransferItems
|
||||
{
|
||||
public function loadStockTransferItems(): Collection
|
||||
{
|
||||
return $this->stockTransferItems = StockTransferItem::query()
|
||||
->currentUser()
|
||||
->where('user_id', auth()->id())
|
||||
->whereNull('stock_transfer_id')
|
||||
->latest()
|
||||
->get();
|
||||
}
|
||||
}
|
||||
169
app/Traits/StockTransfer/WithStockTransferStock.php
Normal file
169
app/Traits/StockTransfer/WithStockTransferStock.php
Normal file
@ -0,0 +1,169 @@
|
||||
<?php
|
||||
|
||||
namespace App\Traits\StockTransfer;
|
||||
|
||||
use App\Models\Bottle;
|
||||
use App\Models\Perfume;
|
||||
use App\Models\Product;
|
||||
use App\Models\StockTransfer;
|
||||
use App\Services\StockActivityLogService;
|
||||
|
||||
trait WithStockTransferStock
|
||||
{
|
||||
public function transferStockBetweenOutlets($sourceOutlet, $destinationOutlet, $item): void
|
||||
{
|
||||
$quantity = $item->quantity;
|
||||
|
||||
switch ($item->transferable_type) {
|
||||
case Perfume::class:
|
||||
$relation = 'perfumes';
|
||||
break;
|
||||
case Product::class:
|
||||
$relation = 'products';
|
||||
break;
|
||||
case Bottle::class:
|
||||
$relation = 'bottles';
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
// Decrease source outlet stock
|
||||
$sourceItem = $sourceOutlet->{$relation}()
|
||||
->where("{$relation}.id", $item->transferable_id)
|
||||
->lockForUpdate()
|
||||
->withPivot('stock')
|
||||
->first();
|
||||
|
||||
$sourcePreviousStock = 0;
|
||||
|
||||
if ($sourceItem) {
|
||||
$sourcePreviousStock = $sourceItem->pivot->stock ?? 0;
|
||||
}
|
||||
|
||||
if ($sourcePreviousStock < $quantity) {
|
||||
throw new \Exception("Stok {$item->transferable->name} di outlet asal tidak mencukupi. Tersisa {$sourcePreviousStock}, diminta {$quantity}.");
|
||||
}
|
||||
|
||||
$sourceNewStock = $sourcePreviousStock - $quantity;
|
||||
|
||||
$sourceOutlet->{$relation}()->updateExistingPivot($item->transferable_id, [
|
||||
'stock' => $sourceNewStock,
|
||||
]);
|
||||
|
||||
// Increase destination outlet stock
|
||||
$destinationItem = $destinationOutlet->{$relation}()
|
||||
->where("{$relation}.id", $item->transferable_id)
|
||||
->lockForUpdate()
|
||||
->withPivot('stock')
|
||||
->first();
|
||||
|
||||
if ($destinationItem) {
|
||||
$destinationPreviousStock = $destinationItem->pivot->stock ?? 0;
|
||||
$destinationOutlet->{$relation}()->updateExistingPivot($item->transferable_id, [
|
||||
'stock' => $destinationPreviousStock + $quantity,
|
||||
]);
|
||||
} else {
|
||||
$destinationPreviousStock = 0;
|
||||
$destinationOutlet->{$relation}()->attach($item->transferable_id, [
|
||||
'stock' => $quantity,
|
||||
]);
|
||||
}
|
||||
$destinationNewStock = $destinationPreviousStock + $quantity;
|
||||
|
||||
$stockTransfer = $item->stockTransfer ?? StockTransfer::find($item->stock_transfer_id);
|
||||
|
||||
if ($stockTransfer) {
|
||||
StockActivityLogService::transfer(
|
||||
stockTransfer: $stockTransfer,
|
||||
product: $item->transferable,
|
||||
quantity: $quantity,
|
||||
sourcePreviousStock: $sourcePreviousStock,
|
||||
sourceNewStock: $sourceNewStock,
|
||||
destinationPreviousStock: $destinationPreviousStock,
|
||||
destinationNewStock: $destinationNewStock
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function reverseStockTransferBetweenOutlets($sourceOutlet, $destinationOutlet, $item): void
|
||||
{
|
||||
$quantity = $item->quantity;
|
||||
|
||||
switch ($item->transferable_type) {
|
||||
case Perfume::class:
|
||||
$relation = 'perfumes';
|
||||
break;
|
||||
case Product::class:
|
||||
$relation = 'products';
|
||||
break;
|
||||
case Bottle::class:
|
||||
$relation = 'bottles';
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
// Return items to source outlet
|
||||
$sourcePivot = $sourceOutlet->{$relation}()
|
||||
->where("{$relation}.id", $item->transferable_id)
|
||||
->first();
|
||||
|
||||
$currentSourceStock = $sourcePivot?->pivot?->stock ?? 0;
|
||||
$newSourceStock = $currentSourceStock + $quantity;
|
||||
|
||||
if ($sourcePivot) {
|
||||
$sourceOutlet->{$relation}()->updateExistingPivot($item->transferable_id, [
|
||||
'stock' => $newSourceStock,
|
||||
]);
|
||||
} else {
|
||||
$sourceOutlet->{$relation}()->attach($item->transferable_id, [
|
||||
'stock' => $quantity,
|
||||
]);
|
||||
}
|
||||
|
||||
$stockTransfer = $item->stockTransfer ?? StockTransfer::find($item->stock_transfer_id);
|
||||
|
||||
if ($stockTransfer) {
|
||||
StockActivityLogService::logOutlet(
|
||||
logName: StockActivityLogService::LOG_TRANSFER,
|
||||
event: 'increase',
|
||||
description: "Stock Transfer #{$stockTransfer->hash_id} Deleted: Returned {$quantity} stock to source outlet.",
|
||||
performedOn: $item->transferable,
|
||||
outlet: $sourceOutlet,
|
||||
quantityChange: $quantity,
|
||||
previousStock: $currentSourceStock,
|
||||
newStock: $newSourceStock,
|
||||
extraProperties: ['stock_transfer_id' => $stockTransfer->id]
|
||||
);
|
||||
}
|
||||
|
||||
// Remove items from destination outlet
|
||||
$destinationPivot = $destinationOutlet->{$relation}()
|
||||
->where("{$relation}.id", $item->transferable_id)
|
||||
->first();
|
||||
|
||||
$currentDestinationStock = $destinationPivot?->pivot?->stock ?? 0;
|
||||
$newDestinationStock = max(0, $currentDestinationStock - $quantity);
|
||||
|
||||
if ($destinationPivot) {
|
||||
$destinationOutlet->{$relation}()->updateExistingPivot($item->transferable_id, [
|
||||
'stock' => $newDestinationStock,
|
||||
]);
|
||||
}
|
||||
|
||||
if ($stockTransfer) {
|
||||
StockActivityLogService::logOutlet(
|
||||
logName: StockActivityLogService::LOG_TRANSFER,
|
||||
event: 'decrease',
|
||||
description: "Stock Transfer #{$stockTransfer->hash_id} Deleted: Removed {$quantity} stock from destination outlet.",
|
||||
performedOn: $item->transferable,
|
||||
outlet: $destinationOutlet,
|
||||
quantityChange: -1 * abs($quantity),
|
||||
previousStock: $currentDestinationStock,
|
||||
newStock: $newDestinationStock,
|
||||
extraProperties: ['stock_transfer_id' => $stockTransfer->id]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
84
app/Traits/StockTransfer/WithStockTransferUpdateItem.php
Normal file
84
app/Traits/StockTransfer/WithStockTransferUpdateItem.php
Normal file
@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace App\Traits\StockTransfer;
|
||||
|
||||
use App\Models\Bottle;
|
||||
use App\Models\Outlet;
|
||||
use App\Models\Perfume;
|
||||
use App\Models\Product;
|
||||
use App\Models\StockTransferItem;
|
||||
use Livewire\Attributes\On;
|
||||
|
||||
trait WithStockTransferUpdateItem
|
||||
{
|
||||
public string $modalTitle = '';
|
||||
|
||||
public function updateItem(): void
|
||||
{
|
||||
$this->canOrAbort('create stock transfer');
|
||||
|
||||
if (! $this->form->source_outlet_id) {
|
||||
$this->toast('Silakan pilih outlet asal terlebih dahulu.', 'Gagal', 'warning');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$sourceOutlet = Outlet::find($this->form->source_outlet_id);
|
||||
|
||||
if (! $sourceOutlet) {
|
||||
$this->toast('Outlet asal tidak ditemukan.', 'Gagal', 'danger');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$item = StockTransferItem::find($this->form->item_id);
|
||||
|
||||
if ($item) {
|
||||
$newQuantity = parseRupiahToInt($this->form->quantity_edit);
|
||||
|
||||
if ($newQuantity <= 0) {
|
||||
$this->toast('Jumlah harus lebih dari 0.', 'Gagal', 'danger');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$relationName = match ($item->transferable_type) {
|
||||
Perfume::class => 'perfumes',
|
||||
Product::class => 'products',
|
||||
Bottle::class => 'bottles',
|
||||
default => null,
|
||||
};
|
||||
|
||||
if ($relationName) {
|
||||
$sourceStock = $sourceOutlet->{$relationName}()
|
||||
->where($item->transferable_type::make()->getTable().'.id', $item->transferable_id)
|
||||
->first()
|
||||
?->pivot
|
||||
?->stock ?? 0;
|
||||
|
||||
if ($newQuantity > $sourceStock) {
|
||||
$this->toast("Stok tidak mencukupi (Update). Stok di outlet asal: {$sourceStock}.", 'Gagal', 'danger');
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$item->update([
|
||||
'quantity' => $newQuantity,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->stockTransferItems = $this->loadStockTransferItems();
|
||||
$this->toast('Item berhasil diperbarui.');
|
||||
$this->dispatch('modal:close', 'form-modal');
|
||||
}
|
||||
|
||||
#[On('modal:open')]
|
||||
public function openModal($item): void
|
||||
{
|
||||
$item = StockTransferItem::find($item['item']);
|
||||
$this->form->item_id = $item->id;
|
||||
$this->form->quantity_edit = formatCurrencyNumber($item->quantity, '');
|
||||
$this->modalTitle = $item->transferable->name;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
<?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_transfers', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('source_outlet_id')->constrained('outlets')->cascadeOnDelete();
|
||||
$table->foreignId('destination_outlet_id')->constrained('outlets')->cascadeOnDelete();
|
||||
$table->date('transfer_date');
|
||||
$table->string('note', 100)->nullable();
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
$table->timestamp('updated_at')->nullable()->useCurrentOnUpdate();
|
||||
$table->softDeletes();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('stock_transfers');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,33 @@
|
||||
<?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_transfer_items', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('stock_transfer_id')->nullable()->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->morphs('transferable');
|
||||
$table->unsignedInteger('quantity');
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
$table->timestamp('updated_at')->nullable()->useCurrentOnUpdate();
|
||||
$table->softDeletes();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('stock_transfer_items');
|
||||
}
|
||||
};
|
||||
@ -139,6 +139,11 @@ public function run(): void
|
||||
'update restock',
|
||||
'delete restock',
|
||||
|
||||
'view stock transfer',
|
||||
'create stock transfer',
|
||||
'update stock transfer',
|
||||
'delete stock transfer',
|
||||
|
||||
'view reward',
|
||||
'create reward',
|
||||
'update reward',
|
||||
@ -399,6 +404,11 @@ public function run(): void
|
||||
'update restock',
|
||||
'delete restock',
|
||||
|
||||
'view stock transfer',
|
||||
'create stock transfer',
|
||||
'update stock transfer',
|
||||
'delete stock transfer',
|
||||
|
||||
'view reward',
|
||||
'create reward',
|
||||
'update reward',
|
||||
|
||||
@ -0,0 +1,213 @@
|
||||
<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_transfer.index') }}" wire:navigate class="text-sm">
|
||||
Kembali
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<flux:callout icon="megaphone" color="blue" class="mb-6">
|
||||
<flux:callout.heading>Informasi Transfer Stok</flux:callout.heading>
|
||||
<flux:callout.text>
|
||||
Proses transfer stok akan **mengurangi** stok di Outlet Asal dan **menambah** stok di Outlet Tujuan yang dipilih.
|
||||
</flux:callout.text>
|
||||
</flux:callout>
|
||||
|
||||
<div class="flex flex-col lg:flex-row gap-4">
|
||||
<div class="w-full lg:w-3/4 space-y-4">
|
||||
<flux:card class="space-y-6 p-6">
|
||||
<flux:input label="Catatan" placeholder="Transfer stok antar outlet" wire:model="form.note"
|
||||
autocomplete="off" />
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<flux:field>
|
||||
<flux:label>Tanggal Transfer <span class="text-red-500 ms-1">*</span></flux:label>
|
||||
<flux:date-picker with-today wire:model="form.transfer_date" placeholder="Pilih Tanggal"
|
||||
autocomplete="off" locale="id-ID" selectable-header />
|
||||
<flux:error name="form.transfer_date" />
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>Outlet Asal <span class="text-red-500 ms-1">*</span></flux:label>
|
||||
<flux:select variant="listbox" searchable placeholder="Pilih Outlet Asal"
|
||||
wire:model.live="form.source_outlet_id">
|
||||
@foreach ($sourceOutlets as $key => $value)
|
||||
<flux:select.option value="{{ $key }}">{{ $value }}
|
||||
</flux:select.option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
<flux:error name="form.source_outlet_id" />
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>Outlet Tujuan <span class="text-red-500 ms-1">*</span></flux:label>
|
||||
<flux:select variant="listbox" searchable placeholder="Pilih Outlet Tujuan"
|
||||
wire:model.live="form.destination_outlet_id">
|
||||
@foreach ($destinationOutlets as $key => $value)
|
||||
<flux:select.option value="{{ $key }}">{{ $value }}
|
||||
</flux:select.option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
<flux:error name="form.destination_outlet_id" />
|
||||
</flux:field>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
@if ($form->source_outlet_id && $form->destination_outlet_id)
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<flux:card class="space-y-6 p-6">
|
||||
<flux:select label="Parfum" variant="listbox" searchable placeholder="Cari parfum..."
|
||||
wire:model="form.perfume_id">
|
||||
@foreach ($perfumes as $key => $perfume)
|
||||
<flux:select.option value="{{ $key }}">{{ $perfume }}
|
||||
</flux:select.option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
<flux:input label="Kuantitas" placeholder="500" wire:model="form.quantity_perfume"
|
||||
autocomplete="off" x-mask:dynamic="$money($input, ',')" />
|
||||
|
||||
<div class="flex justify-end">
|
||||
<flux:button variant="primary" color="zinc" wire:click="addItem('parfum')">
|
||||
Tambah
|
||||
</flux:button>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
<flux:card class="space-y-6 p-6">
|
||||
<flux:select label="Produk" variant="listbox" searchable placeholder="Cari produk..."
|
||||
wire:model="form.product_id">
|
||||
@foreach ($products as $key => $product)
|
||||
<flux:select.option value="{{ $key }}">{{ $product }}
|
||||
</flux:select.option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
<flux:input label="Kuantitas" placeholder="10" wire:model="form.quantity_product"
|
||||
autocomplete="off" x-mask:dynamic="$money($input, ',')" />
|
||||
|
||||
<div class="flex justify-end">
|
||||
<flux:button variant="primary" color="zinc" wire:click="addItem('produk')">
|
||||
Tambah
|
||||
</flux:button>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
<flux:card class="space-y-6 p-6">
|
||||
<flux:select label="Botol" variant="listbox" searchable placeholder="Cari botol..."
|
||||
wire:model="form.bottle_id">
|
||||
@foreach ($bottles as $key => $bottle)
|
||||
<flux:select.option value="{{ $key }}">{{ $bottle }}
|
||||
</flux:select.option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
<flux:input label="Kuantitas" placeholder="50" wire:model="form.quantity_bottle"
|
||||
autocomplete="off" x-mask:dynamic="$money($input, ',')" />
|
||||
|
||||
<div class="flex justify-end">
|
||||
<flux:button variant="primary" color="zinc" wire:click="addItem('botol')">
|
||||
Tambah
|
||||
</flux:button>
|
||||
</div>
|
||||
</flux:card>
|
||||
</div>
|
||||
@else
|
||||
<flux:card class="p-6 text-center text-zinc-500 italic">
|
||||
Silakan pilih Outlet Asal dan Outlet Tujuan terlebih dahulu untuk menampilkan daftar item.
|
||||
</flux:card>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="w-full lg:w-1/3 space-y-4">
|
||||
<flux:card class="space-y-6 p-6">
|
||||
<div class="space-y-3">
|
||||
<h3 class="text-sm font-medium">Gambar</h3>
|
||||
<div class="dropzone-wrapper">
|
||||
<livewire:dropzone wire:model="form.image" :rules="['image', 'mimes:png,jpeg', 'max:10420']" :max-files="1"
|
||||
:key="'image'" :files="$form->image" />
|
||||
@error('form.image')
|
||||
<div class="mt-3 text-sm font-medium text-red-500 dark:text-red-400">
|
||||
{{ $message }}
|
||||
</div>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
<flux:card class="space-y-6 p-6">
|
||||
<h3 class="text-sm font-medium">Keranjang Transfer Stok</h3>
|
||||
|
||||
<div class="divide-y text-sm">
|
||||
<flux:table>
|
||||
<flux:table.columns>
|
||||
<flux:table.column>Item</flux:table.column>
|
||||
<flux:table.column>Qty</flux:table.column>
|
||||
<flux:table.column>Aksi</flux:table.column>
|
||||
</flux:table.columns>
|
||||
|
||||
<flux:table.rows>
|
||||
@forelse($stockTransferItems as $item)
|
||||
<flux:table.row>
|
||||
<flux:table.cell>
|
||||
<flux:heading>
|
||||
<div>{{ $item->transferable->name }}</div>
|
||||
</flux:heading>
|
||||
</flux:table.cell>
|
||||
<flux:table.cell>
|
||||
{{ formatCurrencyNumber($item->quantity, '') }}
|
||||
</flux:table.cell>
|
||||
<flux:table.cell class="space-x-2">
|
||||
<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', {'item': '{{ $item->id }}'})">
|
||||
</flux:button>
|
||||
</flux:tooltip>
|
||||
</flux:modal.trigger>
|
||||
|
||||
<flux:button variant="danger" icon="trash" size="sm"
|
||||
wire:click="deleteItem('{{ $item->id }}')">
|
||||
</flux:button>
|
||||
</flux:table.cell>
|
||||
</flux:table.row>
|
||||
@empty
|
||||
<flux:table.row>
|
||||
<flux:table.cell colspan="3" class="text-center">
|
||||
<span class="text-sm text-zinc-500 italic">Keranjang Kosong...</span>
|
||||
</flux:table.cell>
|
||||
</flux:table.row>
|
||||
@endforelse
|
||||
</flux:table.rows>
|
||||
</flux:table>
|
||||
</div>
|
||||
</flux:card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-start mt-4">
|
||||
<flux:button variant="primary" class="sm:w-auto cursor-pointer" wire:click="save">
|
||||
Simpan
|
||||
</flux:button>
|
||||
</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">
|
||||
<flux:heading size="lg">Ubah {{ $modalTitle }}</flux:heading>
|
||||
|
||||
<flux:input label="Kuantitas" placeholder="Masukkan kuantitas" wire:model="form.quantity_edit"
|
||||
autocomplete="off" x-mask:dynamic="$money($input, ',')" />
|
||||
|
||||
<div class="flex">
|
||||
<flux:spacer />
|
||||
<flux:button variant="primary" color="zinc" class="sm:w-auto cursor-pointer"
|
||||
wire:click="updateItem">
|
||||
Perbarui
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</flux:modal>
|
||||
</flux:main>
|
||||
@ -0,0 +1,35 @@
|
||||
<flux:main>
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<flux:heading size="xl">{{ $pageTitle }}</flux:heading>
|
||||
</div>
|
||||
@can('create stock transfer')
|
||||
<div>
|
||||
<flux:button href="{{ route('studio.manage.stock_transfer.create') }}" variant="primary" wire:navigate
|
||||
class="text-sm">
|
||||
Tambah
|
||||
</flux:button>
|
||||
</div>
|
||||
@endcan
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<flux:callout icon="megaphone" color="blue" class="mb-6">
|
||||
<flux:callout.heading>Informasi</flux:callout.heading>
|
||||
<flux:callout.text>
|
||||
Menghapus data transfer stok otomatis mengembalikan stok ke outlet asal.
|
||||
</flux:callout.text>
|
||||
</flux:callout>
|
||||
|
||||
<livewire:datatable.manage.stock-transfers-table />
|
||||
</div>
|
||||
|
||||
@include('components.modals.confirmation', [
|
||||
'modalName' => 'delete-confirmation',
|
||||
'modalTitle' => 'Apakah Anda yakin?',
|
||||
'modalMessage' => 'Data transfer stok akan dihapus dari riwayat.',
|
||||
'buttonVariant' => 'primary',
|
||||
'buttonColor' => 'danger',
|
||||
'buttonText' => 'Ya, Hapus',
|
||||
])
|
||||
</flux:main>
|
||||
@ -42,6 +42,8 @@
|
||||
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\Manage\StockTransfer\Create as StockTransferCreate;
|
||||
use App\Livewire\Studio\Manage\StockTransfer\Index as StockTransferIndex;
|
||||
use App\Livewire\Studio\Master\Category as CategoryComponent;
|
||||
use App\Livewire\Studio\Master\Formula as FormulaComponent;
|
||||
use App\Livewire\Studio\Master\Outlet\Create as OutletCreate;
|
||||
@ -172,6 +174,11 @@
|
||||
Route::get('/create', RestockCreate::class)->name('create')->middleware('can:create restock');
|
||||
});
|
||||
|
||||
Route::prefix('stock-transfers')->name('stock_transfer.')->group(function () {
|
||||
Route::get('/', StockTransferIndex::class)->name('index')->middleware('can:view stock transfer');
|
||||
Route::get('/create', StockTransferCreate::class)->name('create')->middleware('can:create stock transfer');
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user