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 '' . ucfirst($row->status) . ''; }) ->addColumn('action', function ($row) use ($menu) { $btn = '
'; $btn .= 'Detail'; $btn .= '
'; 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); } } }