Files
CKB/app/Http/Controllers/WarehouseManagement/OpnamesController.php

293 lines
11 KiB
PHP

<?php
namespace App\Http\Controllers\WarehouseManagement;
use App\Http\Controllers\Controller;
use App\Models\Dealer;
use App\Models\Menu;
use App\Models\Opname;
use App\Models\OpnameDetail;
use App\Models\Product;
use App\Models\Stock;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Yajra\DataTables\Facades\DataTables;
class OpnamesController extends Controller
{
public function index(Request $request){
$menu = Menu::where('link','opnames.index')->first();
if($request->ajax()){
$data = Opname::with('user','dealer')
->orderBy('created_at', 'desc')
->get();
return DataTables::of($data)
->addColumn('user_name', function ($row){
return $row->user ? $row->user->name : '-';
})
->addColumn('dealer_name', function ($row){
return $row->dealer ? $row->dealer->name : '-';
})
->editColumn('opname_date', function ($row){
return $row->opname_date ? Carbon::parse($row->opname_date)->format('d M Y') : '-';
})
->editColumn('created_at', function ($row) {
return Carbon::parse($row->created_at)->format('d M Y H:i');
})
->editColumn('status', function ($row) {
$statusClass = [
'draft' => 'warning',
'pending' => 'info',
'approved' => 'success',
'rejected' => 'danger'
][$row->status] ?? 'secondary';
return '<span class="badge badge-' . $statusClass . '">' . ucfirst($row->status) . '</span>';
})
->addColumn('action', function ($row) use ($menu) {
$btn = '<div class="d-flex">';
$btn .= '<a href="'.route('opnames.show', $row->id).'" class="btn btn-secondary btn-sm">Detail</a>';
$btn .= '</div>';
return $btn;
})
->rawColumns(['action', 'status'])
->make(true);
}
return view('warehouse_management.opnames.index');
}
public function create(){
try{
$dealers = Dealer::all();
$products = Product::where('active', true)->get();
// Get initial stock data for the first dealer (if any)
$initialDealerId = $dealers->first()?->id;
$stocks = [];
if ($initialDealerId) {
$stocks = Stock::where('dealer_id', $initialDealerId)
->whereIn('product_id', $products->pluck('id'))
->get()
->keyBy('product_id');
}
return view('warehouse_management.opnames.create', compact('dealers', 'products', 'stocks'));
} catch(\Exception $ex) {
Log::error($ex->getMessage());
return back()->with('error', 'Terjadi kesalahan saat memuat data');
}
}
public function store(Request $request)
{
try {
DB::beginTransaction();
// 1. Validasi input
$validated = $request->validate([
'dealer' => 'required|exists:dealers,id',
'product' => 'required|array|min:1',
'product.*' => 'required|exists:products,id',
'system_quantity' => 'required|array',
'system_quantity.*' => 'required|numeric|min:0',
'physical_quantity' => 'required|array',
'physical_quantity.*' => 'required|numeric|min:0',
'note' => 'nullable|string|max:1000', // note utama
'item_notes' => 'nullable|array', // notes per item
'item_notes.*' => 'required_if:physical_quantity.*,!=,system_quantity.*|nullable|string|max:255'
]);
// 2. Validasi duplikasi produk
$productCounts = array_count_values(array_filter($request->product));
foreach ($productCounts as $productId => $count) {
if ($count > 1) {
throw new \Exception('Product tidak boleh duplikat.');
}
}
// 3. Validasi dealer
$dealer = Dealer::findOrFail($request->dealer);
// 4. Validasi produk aktif
$productIds = array_filter($request->product);
$inactiveProducts = Product::whereIn('id', $productIds)
->where('active', false)
->pluck('name')
->toArray();
if (!empty($inactiveProducts)) {
throw new \Exception('Produk berikut tidak aktif: ' . implode(', ', $inactiveProducts));
}
// 5. Validasi stock dan note
$stockDifferences = [];
foreach ($request->product as $index => $productId) {
if (!$productId) continue;
$systemStock = floatval($request->system_quantity[$index] ?? 0);
$physicalStock = floatval($request->physical_quantity[$index] ?? 0);
$itemNote = $request->input("item_notes.{$index}");
// Jika ada perbedaan stock dan note kosong
if (abs($systemStock - $physicalStock) > 0.01 && empty($itemNote)) {
$product = Product::find($productId);
$stockDifferences[] = $product->name;
}
}
if (!empty($stockDifferences)) {
throw new \Exception(
'Catatan harus diisi untuk produk berikut karena ada perbedaan stock: ' .
implode(', ', $stockDifferences)
);
}
// 6. Create Opname master record with approved status
$opname = Opname::create([
'dealer_id' => $request->dealer,
'opname_date' => now(),
'user_id' => auth()->id(),
'note' => $request->note,
'status' => 'approved', // Set status langsung approved
'approved_by' => auth()->id(), // Set current user sebagai approver
'approved_at' => now() // Set waktu approval
]);
// 7. Create OpnameDetails and update stock
$details = [];
foreach ($request->product as $index => $productId) {
if (!$productId) continue;
$systemStock = floatval($request->system_quantity[$index] ?? 0);
$physicalStock = floatval($request->physical_quantity[$index] ?? 0);
$difference = $physicalStock - $systemStock;
// Create opname detail
$details[] = [
'opname_id' => $opname->id,
'product_id' => $productId,
'system_stock' => $systemStock,
'physical_stock' => $physicalStock,
'difference' => $difference,
'note' => $request->input("item_notes.{$index}"),
'created_at' => now(),
'updated_at' => now()
];
// Update stock langsung karena auto approve
$stock = Stock::firstOrCreate(
[
'product_id' => $productId,
'dealer_id' => $request->dealer
],
['quantity' => 0]
);
// Update stock dengan physical stock
$stock->updateStock(
$physicalStock,
$opname,
"Stock adjustment from auto-approved opname #{$opname->id}"
);
}
// Bulk insert untuk performa lebih baik
OpnameDetail::insert($details);
// 8. Log aktivitas
Log::info('Opname created and auto-approved', [
'opname_id' => $opname->id,
'dealer_id' => $opname->dealer_id,
'user_id' => auth()->id(),
'approver_id' => auth()->id(),
'product_count' => count($details)
]);
DB::commit();
return redirect()
->route('opnames.index')
->with('success', 'Opname berhasil disimpan dan disetujui.');
} catch (\Illuminate\Validation\ValidationException $e) {
DB::rollBack();
return back()->withErrors($e->validator)->withInput();
} catch (\Exception $e) {
DB::rollBack();
Log::error('Error in OpnamesController@store: ' . $e->getMessage());
Log::error($e->getTraceAsString());
return back()
->with('error', $e->getMessage())
->withInput();
}
}
public function show(Request $request, $id)
{
try {
$opname = Opname::with('details.product', 'user')->findOrFail($id);
if ($request->ajax()) {
return DataTables::of($opname->details)
->addIndexColumn()
->addColumn('opname_date', function () use ($opname) {
return Carbon::parse($opname->opname_date)->format('d M Y');
})
->addColumn('user_name', function () use ($opname) {
return $opname->user ? $opname->user->name : '-';
})
->addColumn('product_name', function ($detail) {
return $detail->product->name ?? '-';
})
->addColumn('system_stock', function ($detail) {
return $detail->system_stock;
})
->addColumn('physical_stock', function ($detail) {
return $detail->physical_stock;
})
->addColumn('difference', function ($detail) {
return $detail->difference;
})
->make(true);
}
return view('warehouse_management.opnames.detail', compact('opname'));
} catch (\Exception $ex) {
Log::error($ex->getMessage());
abort(500, 'Something went wrong');
}
}
// Add new method to get stock data via AJAX
public function getStockData(Request $request)
{
try {
$dealerId = $request->dealer_id;
$productIds = $request->product_ids;
if (!$dealerId || !$productIds) {
return response()->json(['error' => 'Dealer ID dan Product IDs diperlukan'], 400);
}
$stocks = Stock::where('dealer_id', $dealerId)
->whereIn('product_id', $productIds)
->get()
->mapWithKeys(function ($stock) {
return [$stock->product_id => $stock->quantity];
});
return response()->json(['stocks' => $stocks]);
} catch (\Exception $e) {
Log::error('Error getting stock data: ' . $e->getMessage());
return response()->json(['error' => 'Terjadi kesalahan saat mengambil data stok'], 500);
}
}
}