done hosting
This commit is contained in:
parent
64e9140db7
commit
1872cc04c2
|
@ -11,7 +11,9 @@
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
|
|
||||||
<div class="container-fluid mb-4">
|
<div class="container-fluid mb-4">
|
||||||
|
<h4>Sesuaikan Stok</h4>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<livewire:search-product/>
|
<livewire:search-product/>
|
||||||
|
|
|
@ -7,10 +7,9 @@ use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Modules\StockTransfer\Entities\StockTransfer;
|
use Modules\StockTransfer\Entities\StockTransfer;
|
||||||
use Modules\StockTransfer\Entities\StockTransferItem;
|
use Modules\StockTransfer\Entities\StockTransferItem;
|
||||||
use Modules\StockTransfer\Http\Requests\StockTransferRequest;
|
|
||||||
use App\Models\ProductBatch;
|
|
||||||
use Modules\Branch\Entities\Branch;
|
use Modules\Branch\Entities\Branch;
|
||||||
use Modules\Product\Entities\Product;
|
use Modules\Product\Entities\Product;
|
||||||
|
use App\Models\ProductBatch;
|
||||||
|
|
||||||
class StockTransferController extends Controller
|
class StockTransferController extends Controller
|
||||||
{
|
{
|
||||||
|
@ -27,72 +26,80 @@ class StockTransferController extends Controller
|
||||||
{
|
{
|
||||||
$branches = Branch::all();
|
$branches = Branch::all();
|
||||||
$products = Product::all();
|
$products = Product::all();
|
||||||
|
|
||||||
return view('stocktransfer::create', compact('branches', 'products'));
|
return view('stocktransfer::create', compact('branches', 'products'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function store(StockTransferRequest $request)
|
public function store(Request $request)
|
||||||
{
|
{
|
||||||
|
$data = $request->validate([
|
||||||
|
'from_branch_id' => 'required|exists:branches,id',
|
||||||
|
'to_branch_id' => 'required|exists:branches,id|different:from_branch_id',
|
||||||
|
'items' => 'required|array|min:1',
|
||||||
|
'items.*.product_id' => 'required|exists:products,id',
|
||||||
|
'items.*.product_batch_id' => 'required|exists:product_batches,id',
|
||||||
|
'items.*.quantity' => 'required|integer|min:1',
|
||||||
|
]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
DB::beginTransaction();
|
DB::beginTransaction();
|
||||||
|
|
||||||
// Create stock transfer record
|
|
||||||
$transfer = StockTransfer::create([
|
$transfer = StockTransfer::create([
|
||||||
'source_branch_id' => $request->source_branch_id,
|
'source_branch_id' => $data['from_branch_id'],
|
||||||
'destination_branch_id' => $request->destination_branch_id,
|
'destination_branch_id' => $data['to_branch_id'],
|
||||||
'transfer_date' => $request->transfer_date,
|
'transfer_date' => now(),
|
||||||
'note' => $request->note,
|
'status' => 'completed',
|
||||||
'status' => 'pending',
|
|
||||||
'created_by' => auth()->id(),
|
'created_by' => auth()->id(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Process each transfer item
|
foreach ($data['items'] as $item) {
|
||||||
foreach ($request->items as $item) {
|
$sourceBatch = ProductBatch::where('id', $item['product_batch_id'])
|
||||||
// Get source batch
|
->where('branch_id', $data['from_branch_id'])
|
||||||
$sourceBatch = ProductBatch::findOrFail($item['product_batch_id']);
|
->lockForUpdate()
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
// Step 1: Deduct quantity from source branch
|
if ($sourceBatch->qty < $item['quantity']) {
|
||||||
$sourceBatch->quantity -= $item['quantity'];
|
throw new \Exception("Stok tidak mencukupi untuk batch {$sourceBatch->batch_code}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kurangi stok dari batch asal
|
||||||
|
$sourceBatch->qty -= $item['quantity'];
|
||||||
$sourceBatch->save();
|
$sourceBatch->save();
|
||||||
|
|
||||||
// Step 2: Create new batch in destination branch
|
// Tambahkan ke batch cabang tujuan (dengan batch_code sama)
|
||||||
ProductBatch::create([
|
$targetBatch = ProductBatch::firstOrCreate(
|
||||||
'product_id' => $sourceBatch->product_id,
|
[
|
||||||
'branch_id' => $request->destination_branch_id,
|
'product_id' => $item['product_id'],
|
||||||
'batch_code' => $sourceBatch->batch_code,
|
'branch_id' => $data['to_branch_id'],
|
||||||
'quantity' => $item['quantity'],
|
'batch_code' => $sourceBatch->batch_code,
|
||||||
'purchase_price' => $sourceBatch->purchase_price,
|
],
|
||||||
'expired_date' => $sourceBatch->expired_date,
|
[
|
||||||
'created_by' => auth()->id(),
|
'qty' => 0,
|
||||||
'updated_by' => auth()->id()
|
'unit_price' => $sourceBatch->unit_price,
|
||||||
]);
|
'price' => $sourceBatch->price,
|
||||||
|
'exp_date' => $sourceBatch->exp_date,
|
||||||
|
'created_by' => auth()->id(),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
// Step 3: Create transfer item record
|
$targetBatch->qty += $item['quantity'];
|
||||||
StockTransferItem::create([
|
$targetBatch->updated_by = auth()->id();
|
||||||
'stock_transfer_id' => $transfer->id,
|
$targetBatch->save();
|
||||||
'product_id' => $sourceBatch->product_id,
|
|
||||||
'product_batch_id' => $sourceBatch->id,
|
// Simpan ke tabel stock_transfer_items
|
||||||
'quantity' => $item['quantity']
|
$transfer->items()->create([
|
||||||
|
'product_id' => $item['product_id'],
|
||||||
|
'batch_id' => $sourceBatch->id,
|
||||||
|
'qty' => $item['quantity'],
|
||||||
|
'unit_price' => $sourceBatch->unit_price,
|
||||||
|
'price' => $sourceBatch->price,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update transfer status to completed
|
|
||||||
$transfer->update([
|
|
||||||
'status' => 'completed',
|
|
||||||
'updated_by' => auth()->id()
|
|
||||||
]);
|
|
||||||
|
|
||||||
DB::commit();
|
DB::commit();
|
||||||
|
return redirect()->route('stock-transfers.index')->with('success', 'Transfer stok berhasil.');
|
||||||
return redirect()
|
|
||||||
->route('stock-transfers.show', $transfer)
|
|
||||||
->with('success', 'Stock transfer created successfully.');
|
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
DB::rollBack();
|
DB::rollBack();
|
||||||
return redirect()
|
return back()->with('error', 'Gagal transfer stok: ' . $e->getMessage())->withInput();
|
||||||
->back()
|
|
||||||
->with('error', 'Error creating stock transfer: ' . $e->getMessage())
|
|
||||||
->withInput();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,9 +113,9 @@ class StockTransferController extends Controller
|
||||||
{
|
{
|
||||||
$batches = ProductBatch::where('product_id', $productId)
|
$batches = ProductBatch::where('product_id', $productId)
|
||||||
->where('branch_id', $branchId)
|
->where('branch_id', $branchId)
|
||||||
->where('quantity', '>', 0)
|
->where('qty', '>', 0)
|
||||||
->get(['id', 'batch_code', 'quantity']);
|
->get(['id', 'batch_code', 'qty']);
|
||||||
|
|
||||||
return response()->json($batches);
|
return response()->json($batches);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,252 +1,87 @@
|
||||||
@extends('layouts.app')
|
@extends('layouts.app')
|
||||||
|
|
||||||
@section('title', 'Create Stock Transfer')
|
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="container-fluid">
|
<div class="container">
|
||||||
<div class="row">
|
<h4>Tambah Transfer Stok</h4>
|
||||||
<div class="col-12">
|
|
||||||
<div class="card">
|
@php
|
||||||
<div class="card-header">
|
use Modules\Branch\Entities\Branch;
|
||||||
<h3 class="card-title">Create Stock Transfer</h3>
|
$branch = Branch::find(session('branch_id'));
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<form action="{{ route('stock-transfers.store') }}" method="POST">
|
||||||
|
@csrf
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12 mb-3">
|
||||||
|
<livewire:search-product />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white p-3 rounded shadow-sm">
|
||||||
|
<div class="row mt-3">
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="source_branch_id" style="color: #212529;">Cabang Pengirim</label>
|
||||||
|
<select name="source_branch_id" id="from_branch_id" class="form-control" readonly>
|
||||||
|
@if($branch)
|
||||||
|
<option value="{{ $branch->id }}" selected>{{ $branch->name }}</option>
|
||||||
|
@else
|
||||||
|
<option value="" selected>Pilih Cabang</option>
|
||||||
|
@endif
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
</div>
|
||||||
<form action="{{ route('stock-transfers.store') }}" method="POST" id="stock-transfer-form">
|
<div class="col-md-4">
|
||||||
@csrf
|
<div class="form-group">
|
||||||
<div class="row">
|
<label for="destination_branch_id" style="color: #212529;">Cabang Penerima</label>
|
||||||
<div class="col-md-6">
|
<select name="destination_branch_id" id="to_branch_id" class="form-control" required>
|
||||||
<div class="form-group">
|
<option value="">Pilih Cabang</option>
|
||||||
<label for="source_branch_id">Source Branch <span class="text-danger">*</span></label>
|
@foreach(App\Models\Branch::where('id', '!=', 1)->get() as $branch)
|
||||||
<select name="source_branch_id" id="source_branch_id" class="form-control select2" required>
|
<option value="{{ $branch->id }}">{{ $branch->name }}</option>
|
||||||
<option value="">Select Source Branch</option>
|
@endforeach
|
||||||
@foreach($branches as $branch)
|
</select>
|
||||||
<option value="{{ $branch->id }}">{{ $branch->name }}</option>
|
</div>
|
||||||
@endforeach
|
</div>
|
||||||
</select>
|
<div class="col-md-4">
|
||||||
</div>
|
<div class="form-group">
|
||||||
</div>
|
<label for="transfer_date" style="color: #212529;">Tanggal Transfer</label>
|
||||||
<div class="col-md-6">
|
<input type="date" name="transfer_date" class="form-control" value="{{ date('Y-m-d') }}" required>
|
||||||
<div class="form-group">
|
|
||||||
<label for="destination_branch_id">Destination Branch <span class="text-danger">*</span></label>
|
|
||||||
<select name="destination_branch_id" id="destination_branch_id" class="form-control select2" required>
|
|
||||||
<option value="">Select Destination Branch</option>
|
|
||||||
@foreach($branches as $branch)
|
|
||||||
<option value="{{ $branch->id }}">{{ $branch->name }}</option>
|
|
||||||
@endforeach
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="transfer_date">Transfer Date <span class="text-danger">*</span></label>
|
|
||||||
<input type="date" name="transfer_date" id="transfer_date" class="form-control" value="{{ date('Y-m-d') }}" required>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="note">Note</label>
|
|
||||||
<textarea name="note" id="note" class="form-control" rows="1"></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mt-4">
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-bordered" id="transfer-items-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Product <span class="text-danger">*</span></th>
|
|
||||||
<th>Batch <span class="text-danger">*</span></th>
|
|
||||||
<th>Available Quantity</th>
|
|
||||||
<th>Transfer Quantity <span class="text-danger">*</span></th>
|
|
||||||
<th>Action</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<select name="items[0][product_id]" class="form-control select2 product-select" required>
|
|
||||||
<option value="">Select Product</option>
|
|
||||||
@foreach($products as $product)
|
|
||||||
<option value="{{ $product->id }}">{{ $product->name }}</option>
|
|
||||||
@endforeach
|
|
||||||
</select>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<select name="items[0][product_batch_id]" class="form-control select2 batch-select" required disabled>
|
|
||||||
<option value="">Select Batch</option>
|
|
||||||
</select>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span class="available-quantity">0</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<input type="number" name="items[0][quantity]" class="form-control transfer-quantity" min="1" required disabled>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<button type="button" class="btn btn-danger btn-sm remove-row">
|
|
||||||
<i class="bi bi-trash"></i>
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<button type="button" class="btn btn-success" id="add-row">
|
|
||||||
<i class="bi bi-plus"></i> Add Item
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mt-4">
|
|
||||||
<div class="col-12">
|
|
||||||
<button type="submit" class="btn btn-primary">Create Transfer</button>
|
|
||||||
<a href="{{ route('stock-transfers.index') }}" class="btn btn-secondary">Cancel</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
{{-- Komponen pencarian produk --}}
|
||||||
|
|
||||||
|
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<livewire:stock-transfer.product-table />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="note" style="color: #212529;">Catatan</label>
|
||||||
|
<textarea name="note" class="form-control" rows="2"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="status" style="color: #212529;">Status</label>
|
||||||
|
<select name="status" class="form-control" required>
|
||||||
|
<option value="pending">Pending</option>
|
||||||
|
<option value="completed">Completed</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<button type="submit" class="btn btn-primary">Simpan Transfer</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
@push('scripts')
|
|
||||||
<script>
|
|
||||||
$(document).ready(function() {
|
|
||||||
let rowCount = 1;
|
|
||||||
|
|
||||||
// Initialize Select2
|
|
||||||
$('.select2').select2();
|
|
||||||
|
|
||||||
// Add new row
|
|
||||||
$('#add-row').click(function() {
|
|
||||||
const newRow = `
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<select name="items[${rowCount}][product_id]" class="form-control select2 product-select" required>
|
|
||||||
<option value="">Select Product</option>
|
|
||||||
@foreach($products as $product)
|
|
||||||
<option value="{{ $product->id }}">{{ $product->name }}</option>
|
|
||||||
@endforeach
|
|
||||||
</select>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<select name="items[${rowCount}][product_batch_id]" class="form-control select2 batch-select" required disabled>
|
|
||||||
<option value="">Select Batch</option>
|
|
||||||
</select>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span class="available-quantity">0</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<input type="number" name="items[${rowCount}][quantity]" class="form-control transfer-quantity" min="1" required disabled>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<button type="button" class="btn btn-danger btn-sm remove-row">
|
|
||||||
<i class="bi bi-trash"></i>
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
`;
|
|
||||||
$('#transfer-items-table tbody').append(newRow);
|
|
||||||
initializeRow(rowCount);
|
|
||||||
rowCount++;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Remove row
|
|
||||||
$(document).on('click', '.remove-row', function() {
|
|
||||||
$(this).closest('tr').remove();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize row elements
|
|
||||||
function initializeRow(index) {
|
|
||||||
$(`select[name="items[${index}][product_id]"]`).select2();
|
|
||||||
$(`select[name="items[${index}][product_batch_id]"]`).select2();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle product selection
|
|
||||||
$(document).on('change', '.product-select', function() {
|
|
||||||
const row = $(this).closest('tr');
|
|
||||||
const productId = $(this).val();
|
|
||||||
const batchSelect = row.find('.batch-select');
|
|
||||||
const quantityInput = row.find('.transfer-quantity');
|
|
||||||
const availableQuantity = row.find('.available-quantity');
|
|
||||||
|
|
||||||
if (productId) {
|
|
||||||
const sourceBranchId = $('#source_branch_id').val();
|
|
||||||
if (!sourceBranchId) {
|
|
||||||
toastr.error('Please select source branch first');
|
|
||||||
$(this).val('').trigger('change');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch batches for the selected product and branch
|
|
||||||
$.get(`/stock-transfers/batches/${productId}/${sourceBranchId}`, function(data) {
|
|
||||||
batchSelect.empty().append('<option value="">Select Batch</option>');
|
|
||||||
data.forEach(batch => {
|
|
||||||
batchSelect.append(`<option value="${batch.id}" data-quantity="${batch.quantity}">${batch.batch_number}</option>`);
|
|
||||||
});
|
|
||||||
batchSelect.prop('disabled', false).trigger('change');
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
batchSelect.empty().prop('disabled', true).trigger('change');
|
|
||||||
quantityInput.prop('disabled', true).val('');
|
|
||||||
availableQuantity.text('0');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle batch selection
|
|
||||||
$(document).on('change', '.batch-select', function() {
|
|
||||||
const row = $(this).closest('tr');
|
|
||||||
const quantityInput = row.find('.transfer-quantity');
|
|
||||||
const availableQuantity = row.find('.available-quantity');
|
|
||||||
const selectedOption = $(this).find('option:selected');
|
|
||||||
|
|
||||||
if (selectedOption.val()) {
|
|
||||||
const quantity = selectedOption.data('quantity');
|
|
||||||
availableQuantity.text(quantity);
|
|
||||||
quantityInput.prop('disabled', false).attr('max', quantity);
|
|
||||||
} else {
|
|
||||||
quantityInput.prop('disabled', true).val('');
|
|
||||||
availableQuantity.text('0');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Form submission
|
|
||||||
$('#stock-transfer-form').submit(function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
// Validate source and destination branches are different
|
|
||||||
const sourceBranch = $('#source_branch_id').val();
|
|
||||||
const destinationBranch = $('#destination_branch_id').val();
|
|
||||||
|
|
||||||
if (sourceBranch === destinationBranch) {
|
|
||||||
toastr.error('Source and destination branches cannot be the same');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate quantities
|
|
||||||
let isValid = true;
|
|
||||||
$('.transfer-quantity').each(function() {
|
|
||||||
const quantity = parseInt($(this).val());
|
|
||||||
const max = parseInt($(this).attr('max'));
|
|
||||||
if (quantity > max) {
|
|
||||||
toastr.error('Transfer quantity cannot exceed available quantity');
|
|
||||||
isValid = false;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isValid) {
|
|
||||||
this.submit();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
@endpush
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\StockTransfer;
|
||||||
|
|
||||||
|
use Livewire\Component;
|
||||||
|
use App\Models\ProductBatch;
|
||||||
|
use Modules\StockTransfer\Entities\StockTransfer;
|
||||||
|
use Modules\StockTransfer\Entities\StockTransferItem;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class CreateStockTransfer extends Component
|
||||||
|
{
|
||||||
|
public $source_branch_id;
|
||||||
|
public $destination_branch_id;
|
||||||
|
public $transfer_date;
|
||||||
|
public $note;
|
||||||
|
public $status = 'pending';
|
||||||
|
public $products = []; // format: [['batch_id' => 1, 'qty' => 5], ...]
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('StockTransfer::create');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handleSubmit()
|
||||||
|
{
|
||||||
|
$this->validate([
|
||||||
|
'source_branch_id' => 'required|exists:branches,id',
|
||||||
|
'destination_branch_id' => 'required|exists:branches,id|different:source_branch_id',
|
||||||
|
'transfer_date' => 'required|date',
|
||||||
|
'products' => 'required|array|min:1',
|
||||||
|
'products.*.batch_id' => 'required|exists:product_batches,id',
|
||||||
|
'products.*.qty' => 'required|integer|min:1',
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
// Buat transfer baru
|
||||||
|
$transfer = StockTransfer::create([
|
||||||
|
'reference_no' => 'TRF-' . strtoupper(Str::random(6)),
|
||||||
|
'source_branch_id' => $this->source_branch_id,
|
||||||
|
'destination_branch_id' => $this->destination_branch_id,
|
||||||
|
'transfer_date' => $this->transfer_date,
|
||||||
|
'note' => $this->note,
|
||||||
|
'status' => $this->status,
|
||||||
|
'created_by' => auth()->id(),
|
||||||
|
'updated_by' => auth()->id(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
foreach ($this->products as $product) {
|
||||||
|
$newBatch = ProductBatch::transferToBranch(
|
||||||
|
$product['batch_id'],
|
||||||
|
$this->destination_branch_id,
|
||||||
|
$product['qty'],
|
||||||
|
auth()->id()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Simpan item transfer
|
||||||
|
StockTransferItem::create([
|
||||||
|
'stock_transfer_id' => $transfer->id,
|
||||||
|
'product_id' => $newBatch->product_id,
|
||||||
|
'product_batch_id' => $newBatch->id,
|
||||||
|
'quantity' => $product['qty'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
session()->flash('success', 'Transfer stok berhasil disimpan.');
|
||||||
|
return redirect()->route('stock-transfers.index');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
session()->flash('error', 'Gagal menyimpan transfer: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\StockTransfer;
|
||||||
|
|
||||||
|
use Livewire\Component;
|
||||||
|
use Modules\Product\Entities\Product;
|
||||||
|
use App\Models\ProductBatch;
|
||||||
|
|
||||||
|
|
||||||
|
class ProductTable extends Component
|
||||||
|
{
|
||||||
|
protected $listeners = ['productSelected'];
|
||||||
|
|
||||||
|
public $products = [];
|
||||||
|
public $fromBranchId;
|
||||||
|
|
||||||
|
public function mount($fromBranchId = null)
|
||||||
|
{
|
||||||
|
$this->fromBranchId = $fromBranchId;
|
||||||
|
$this->products = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('livewire.stock-transfer.product-table');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function productSelected($product) {
|
||||||
|
$branch_id = session('branch_id'); // pastikan session branch_id sudah ada
|
||||||
|
$productModel = Product::with(['batches' => function($q) use ($branch_id) {
|
||||||
|
$q->where('branch_id', $branch_id)->where('qty', '>', 0);
|
||||||
|
}])->find($product['id']);
|
||||||
|
|
||||||
|
if (!$productModel) {
|
||||||
|
return session()->flash('message', 'Produk tidak ditemukan!');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cek duplikasi
|
||||||
|
foreach ($this->products as $p) {
|
||||||
|
if ($p['id'] == $productModel->id) {
|
||||||
|
return session()->flash('message', 'Already exists in the product list!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$productArr = $productModel->toArray();
|
||||||
|
$productArr['batches'] = $productModel->batches->toArray();
|
||||||
|
|
||||||
|
$this->products[] = $productArr;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateBatch($key, $batchId)
|
||||||
|
{
|
||||||
|
$product = $this->products[$key]['product'] ?? $this->products[$key];
|
||||||
|
|
||||||
|
$batch = ProductBatch::where('id', $batchId)
|
||||||
|
->where('branch_id', $this->fromBranchId)
|
||||||
|
->first();
|
||||||
|
if ($batch) {
|
||||||
|
$this->products[$key]['selected_batch_id'] = $batch->id;
|
||||||
|
$this->products[$key]['max_qty'] = $batch->qty;
|
||||||
|
// Reset quantity jika melebihi stok
|
||||||
|
if ($this->products[$key]['quantity'] > $batch->qty) {
|
||||||
|
$this->products[$key]['quantity'] = $batch->qty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateQuantity($key, $qty)
|
||||||
|
{
|
||||||
|
$max = $this->products[$key]['max_qty'] ?? 0;
|
||||||
|
$this->products[$key]['quantity'] = min(max(1, (int)$qty), $max);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeProduct($key)
|
||||||
|
{
|
||||||
|
unset($this->products[$key]);
|
||||||
|
$this->products = array_values($this->products); // reindex
|
||||||
|
}
|
||||||
|
}
|
|
@ -148,4 +148,31 @@ class ProductBatch extends Model
|
||||||
|
|
||||||
return round($totalPrice / $qty, 2);
|
return round($totalPrice / $qty, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function transferToBranch(int $sourceBatchId, int $destinationBranchId, int $qty, int $userId): ProductBatch
|
||||||
|
{
|
||||||
|
$sourceBatch = self::lockForUpdate()->findOrFail($sourceBatchId);
|
||||||
|
|
||||||
|
if ($sourceBatch->quantity < $qty) {
|
||||||
|
throw new \Exception("Stok tidak cukup pada batch {$sourceBatch->batch_code}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kurangi stok dari batch asal
|
||||||
|
$sourceBatch->quantity -= $qty;
|
||||||
|
$sourceBatch->save();
|
||||||
|
|
||||||
|
// Buat batch baru di cabang tujuan
|
||||||
|
return self::create([
|
||||||
|
'product_id' => $sourceBatch->product_id,
|
||||||
|
'batch_code' => $sourceBatch->batch_code,
|
||||||
|
'unit_price' => $sourceBatch->unit_price,
|
||||||
|
'price' => $sourceBatch->price,
|
||||||
|
'expired_date' => $sourceBatch->expired_date,
|
||||||
|
'purchase_id' => $sourceBatch->purchase_id,
|
||||||
|
'branch_id' => $destinationBranchId,
|
||||||
|
'quantity' => $qty,
|
||||||
|
'created_by' => $userId,
|
||||||
|
'updated_by' => $userId,
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
<div>
|
||||||
|
{{-- Nothing in the world is as soft and yielding as water. --}}
|
||||||
|
</div>
|
|
@ -0,0 +1,106 @@
|
||||||
|
<div>
|
||||||
|
{{-- Alert Message --}}
|
||||||
|
@if (session()->has('message'))
|
||||||
|
<div class="alert alert-warning alert-dismissible fade show" role="alert">
|
||||||
|
<div class="alert-body">
|
||||||
|
<span>{{ session('message') }}</span>
|
||||||
|
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
{{-- Loading Overlay --}}
|
||||||
|
<div wire:loading.flex class="col-12 position-absolute justify-content-center align-items-center"
|
||||||
|
style="top:0;right:0;left:0;bottom:0;background-color: rgba(255,255,255,0.5);z-index: 99;">
|
||||||
|
<div class="spinner-border text-primary" role="status">
|
||||||
|
<span class="sr-only">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Product Table --}}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-bordered">
|
||||||
|
<thead>
|
||||||
|
<tr class="align-middle text-center">
|
||||||
|
<th>#</th>
|
||||||
|
<th>Nama Produk</th>
|
||||||
|
<th>Kode</th>
|
||||||
|
<th>Batch (Cabang Pengirim)</th>
|
||||||
|
<th>Stok Batch</th>
|
||||||
|
<th>Jumlah Transfer</th>
|
||||||
|
<th>Aksi</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@if (!empty($products))
|
||||||
|
@foreach ($products as $key => $product)
|
||||||
|
@php
|
||||||
|
$productData = $product['product'] ?? $product;
|
||||||
|
$batches = $product['batches'] ?? [];
|
||||||
|
$selectedBatchId = $product['selected_batch_id'] ?? null;
|
||||||
|
$selectedBatch = collect($batches)->firstWhere('id', $selectedBatchId) ?? ($batches[0] ?? null);
|
||||||
|
@endphp
|
||||||
|
<tr>
|
||||||
|
<td class="align-middle text-center">{{ $key + 1 }}</td>
|
||||||
|
|
||||||
|
{{-- Nama Produk --}}
|
||||||
|
<td class="align-middle">{{ $productData['product_name'] }}</td>
|
||||||
|
|
||||||
|
{{-- Kode Produk --}}
|
||||||
|
<td class="align-middle">{{ $productData['product_code'] }}</td>
|
||||||
|
|
||||||
|
{{-- Pilih Batch --}}
|
||||||
|
<td class="align-middle">
|
||||||
|
<select name="product_batch_ids[]" class="form-control" required
|
||||||
|
wire:change="updateBatch({{ $key }}, $event.target.value)">
|
||||||
|
<option value="">Pilih Batch</option>
|
||||||
|
@foreach ($batches as $batch)
|
||||||
|
<option value="{{ $batch['id'] }}" {{ $selectedBatchId == $batch['id'] ? 'selected' : '' }}>
|
||||||
|
{{ $batch['batch_code'] ?? '-' }} | Qty: {{ $batch['qty'] }}
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{{-- Stok Batch --}}
|
||||||
|
<td class="align-middle text-center">
|
||||||
|
<span class="badge badge-info">
|
||||||
|
{{ $selectedBatch['qty'] ?? '0' }} {{ $productData['product_unit'] ?? '' }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{{-- Input Jumlah Transfer --}}
|
||||||
|
<td class="align-middle">
|
||||||
|
<input type="number" name="quantities[]" min="1"
|
||||||
|
max="{{ $selectedBatch['qty'] ?? 0 }}"
|
||||||
|
class="form-control"
|
||||||
|
value="{{ $product['quantity'] ?? 1 }}"
|
||||||
|
wire:change="updateQuantity({{ $key }}, $event.target.value)"
|
||||||
|
{{ empty($selectedBatch) ? 'disabled' : '' }}>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{{-- Hidden Product ID --}}
|
||||||
|
<input type="hidden" name="product_ids[]" value="{{ $productData['id'] }}">
|
||||||
|
|
||||||
|
{{-- Aksi --}}
|
||||||
|
<td class="align-middle text-center">
|
||||||
|
<button type="button" class="btn btn-danger" wire:click="removeProduct({{ $key }})">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
@else
|
||||||
|
<tr>
|
||||||
|
<td colspan="7" class="text-center">
|
||||||
|
<span class="text-danger">Silakan cari & pilih produk!</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endif
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue