done hosting

This commit is contained in:
adeliaala 2025-06-04 20:17:00 +07:00
parent 64e9140db7
commit 1872cc04c2
8 changed files with 429 additions and 292 deletions

View File

@ -11,7 +11,9 @@
@endsection
@section('content')
<div class="container-fluid mb-4">
<h4>Sesuaikan Stok</h4>
<div class="row">
<div class="col-12">
<livewire:search-product/>

View File

@ -7,10 +7,9 @@ use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Modules\StockTransfer\Entities\StockTransfer;
use Modules\StockTransfer\Entities\StockTransferItem;
use Modules\StockTransfer\Http\Requests\StockTransferRequest;
use App\Models\ProductBatch;
use Modules\Branch\Entities\Branch;
use Modules\Product\Entities\Product;
use App\Models\ProductBatch;
class StockTransferController extends Controller
{
@ -27,72 +26,80 @@ class StockTransferController extends Controller
{
$branches = Branch::all();
$products = Product::all();
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 {
DB::beginTransaction();
// Create stock transfer record
$transfer = StockTransfer::create([
'source_branch_id' => $request->source_branch_id,
'destination_branch_id' => $request->destination_branch_id,
'transfer_date' => $request->transfer_date,
'note' => $request->note,
'status' => 'pending',
'source_branch_id' => $data['from_branch_id'],
'destination_branch_id' => $data['to_branch_id'],
'transfer_date' => now(),
'status' => 'completed',
'created_by' => auth()->id(),
]);
// Process each transfer item
foreach ($request->items as $item) {
// Get source batch
$sourceBatch = ProductBatch::findOrFail($item['product_batch_id']);
foreach ($data['items'] as $item) {
$sourceBatch = ProductBatch::where('id', $item['product_batch_id'])
->where('branch_id', $data['from_branch_id'])
->lockForUpdate()
->firstOrFail();
// Step 1: Deduct quantity from source branch
$sourceBatch->quantity -= $item['quantity'];
if ($sourceBatch->qty < $item['quantity']) {
throw new \Exception("Stok tidak mencukupi untuk batch {$sourceBatch->batch_code}");
}
// Kurangi stok dari batch asal
$sourceBatch->qty -= $item['quantity'];
$sourceBatch->save();
// Step 2: Create new batch in destination branch
ProductBatch::create([
'product_id' => $sourceBatch->product_id,
'branch_id' => $request->destination_branch_id,
// Tambahkan ke batch cabang tujuan (dengan batch_code sama)
$targetBatch = ProductBatch::firstOrCreate(
[
'product_id' => $item['product_id'],
'branch_id' => $data['to_branch_id'],
'batch_code' => $sourceBatch->batch_code,
'quantity' => $item['quantity'],
'purchase_price' => $sourceBatch->purchase_price,
'expired_date' => $sourceBatch->expired_date,
],
[
'qty' => 0,
'unit_price' => $sourceBatch->unit_price,
'price' => $sourceBatch->price,
'exp_date' => $sourceBatch->exp_date,
'created_by' => auth()->id(),
'updated_by' => auth()->id()
]);
]
);
// Step 3: Create transfer item record
StockTransferItem::create([
'stock_transfer_id' => $transfer->id,
'product_id' => $sourceBatch->product_id,
'product_batch_id' => $sourceBatch->id,
'quantity' => $item['quantity']
$targetBatch->qty += $item['quantity'];
$targetBatch->updated_by = auth()->id();
$targetBatch->save();
// Simpan ke tabel stock_transfer_items
$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();
return redirect()
->route('stock-transfers.show', $transfer)
->with('success', 'Stock transfer created successfully.');
return redirect()->route('stock-transfers.index')->with('success', 'Transfer stok berhasil.');
} catch (\Exception $e) {
DB::rollBack();
return redirect()
->back()
->with('error', 'Error creating stock transfer: ' . $e->getMessage())
->withInput();
return back()->with('error', 'Gagal transfer stok: ' . $e->getMessage())->withInput();
}
}
@ -106,8 +113,8 @@ class StockTransferController extends Controller
{
$batches = ProductBatch::where('product_id', $productId)
->where('branch_id', $branchId)
->where('quantity', '>', 0)
->get(['id', 'batch_code', 'quantity']);
->where('qty', '>', 0)
->get(['id', 'batch_code', 'qty']);
return response()->json($batches);
}

View File

@ -1,252 +1,87 @@
@extends('layouts.app')
@section('title', 'Create Stock Transfer')
@section('content')
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h3 class="card-title">Create Stock Transfer</h3>
</div>
<div class="card-body">
<form action="{{ route('stock-transfers.store') }}" method="POST" id="stock-transfer-form">
<div class="container">
<h4>Tambah Transfer Stok</h4>
@php
use Modules\Branch\Entities\Branch;
$branch = Branch::find(session('branch_id'));
@endphp
<form action="{{ route('stock-transfers.store') }}" method="POST">
@csrf
<div class="row">
<div class="col-md-6">
<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">Source Branch <span class="text-danger">*</span></label>
<select name="source_branch_id" id="source_branch_id" class="form-control select2" required>
<option value="">Select Source Branch</option>
@foreach($branches as $branch)
<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="col-md-4">
<div class="form-group">
<label for="destination_branch_id" style="color: #212529;">Cabang Penerima</label>
<select name="destination_branch_id" id="to_branch_id" class="form-control" required>
<option value="">Pilih Cabang</option>
@foreach(App\Models\Branch::where('id', '!=', 1)->get() as $branch)
<option value="{{ $branch->id }}">{{ $branch->name }}</option>
@endforeach
</select>
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label for="transfer_date" style="color: #212529;">Tanggal Transfer</label>
<input type="date" name="transfer_date" class="form-control" value="{{ date('Y-m-d') }}" required>
</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="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
<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="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 class="mt-4">
<button type="submit" class="btn btn-primary">Simpan Transfer</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@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

View File

@ -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());
}
}
}

View File

@ -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
}
}

View File

@ -148,4 +148,31 @@ class ProductBatch extends Model
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,
]);
}
}

View File

@ -0,0 +1,3 @@
<div>
{{-- Nothing in the world is as soft and yielding as water. --}}
</div>

View File

@ -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>