first(); $dealers = Dealer::all(); if($request->ajax()){ $data = Opname::query() ->with('user','dealer') ->orderBy('created_at', 'desc'); // Filter berdasarkan dealer yang dipilih if ($request->filled('dealer_filter')) { $data->where('dealer_id', $request->dealer_filter); } // Filter berdasarkan tanggal if ($request->filled('date_from')) { try { $dateFrom = \Carbon\Carbon::parse($request->date_from)->format('Y-m-d'); $data->whereDate('opname_date', '>=', $dateFrom); } catch (\Exception $e) { // Fallback to original format $data->whereDate('opname_date', '>=', $request->date_from); } } if ($request->filled('date_to')) { try { $dateTo = \Carbon\Carbon::parse($request->date_to)->format('Y-m-d'); $data->whereDate('opname_date', '<=', $dateTo); } catch (\Exception $e) { // Fallback to original format $data->whereDate('opname_date', '<=', $request->date_to); } } 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) { $status = $row->status instanceof OpnameStatus ? $row->status : OpnameStatus::from($row->status); $textColorClass = $status->textColorClass(); $label = $status->label(); return "{$label}"; }) ->addColumn('action', function ($row) use ($menu) { $btn = '
'; $btn .= 'Detail'; $btn .= '
'; return $btn; }) ->rawColumns(['action', 'status']) ->make(true); } return view('warehouse_management.opnames.index', compact('dealers')); } 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(); // Check if this is from transaction form or regular opname form $isTransactionForm = $request->has('form') && $request->form === 'opname'; if ($isTransactionForm) { // Custom validation for transaction form $request->validate([ 'dealer_id' => 'required|exists:dealers,id', 'user_id' => 'required|exists:users,id', 'opname_date' => [ 'nullable', 'string', 'date_format:Y-m-d', 'before_or_equal:today' ], 'description' => 'nullable|string|max:1000', 'product_id' => 'required|array|min:1', 'product_id.*' => 'required|exists:products,id', 'system_stock' => 'required|array', 'system_stock.*' => 'required|numeric|min:0', 'physical_stock' => 'required|array', 'physical_stock.*' => 'required|numeric|min:0' ]); // Process transaction form data with proper date parsing $dealerId = $request->dealer_id; $userId = $request->user_id; // Parse opname date (YYYY-MM-DD format) or use today if empty $inputDate = $request->opname_date ?: now()->format('Y-m-d'); Log::info('Parsing opname date', ['input' => $request->opname_date, 'using' => $inputDate]); $opnameDate = Carbon::createFromFormat('Y-m-d', $inputDate); Log::info('Successfully parsed opname date', ['parsed' => $opnameDate->format('Y-m-d')]); $note = $request->description; $productIds = $request->product_id; $systemStocks = $request->system_stock; $physicalStocks = $request->physical_stock; // Log input data untuk debugging Log::info('Transaction form input data', [ 'product_ids' => $productIds, 'system_stocks' => $systemStocks, 'physical_stocks' => $physicalStocks, 'dealer_id' => $dealerId, 'user_id' => $userId ]); } else { // Original validation for regular opname form $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', 'item_notes' => 'nullable|array', 'item_notes.*' => 'nullable|string|max:255', 'opname_date' => 'nullable|date' // Add opname_date validation for regular form ]); // Process regular form data $dealerId = $request->dealer; $userId = auth()->id(); // Use provided date or current date $inputDate = $request->opname_date ?: now()->format('Y-m-d'); $opnameDate = $request->opname_date ? Carbon::createFromFormat('Y-m-d', $inputDate) : now(); $note = $request->note; $productIds = $request->product; $systemStocks = $request->system_quantity; $physicalStocks = $request->physical_quantity; } // 2. Validasi minimal ada produk yang diisi (termasuk nilai 0) $validProductIds = array_filter($productIds); $validSystemStocks = array_filter($systemStocks, function($value) { return $value !== null && $value !== ''; }); $validPhysicalStocks = array_filter($physicalStocks, function($value) { return $value !== null && $value !== '' && is_numeric($value); }); if (empty($validProductIds) || count($validProductIds) === 0) { throw new \Exception('Minimal harus ada satu produk yang diisi untuk opname.'); } if (count($validPhysicalStocks) === 0) { throw new \Exception('Minimal harus ada satu stock fisik yang diisi (termasuk nilai 0).'); } // 3. Validasi duplikasi produk $productCounts = array_count_values($validProductIds); foreach ($productCounts as $productId => $count) { if ($count > 1) { throw new \Exception('Produk tidak boleh duplikat dalam satu opname.'); } } // 4. Validasi dealer $dealer = Dealer::findOrFail($dealerId); // 5. Validasi user exists $user = User::findOrFail($userId); // 6. Validasi produk aktif $filteredProductIds = array_filter($productIds); $inactiveProducts = Product::whereIn('id', $filteredProductIds) ->where('active', false) ->pluck('name') ->toArray(); if (!empty($inactiveProducts)) { throw new \Exception('Produk berikut tidak aktif: ' . implode(', ', $inactiveProducts)); } // 7. Validasi stock difference (for transaction form, we'll allow any difference without note requirement) $stockDifferences = []; if (!$isTransactionForm) { // Only validate notes for regular opname form foreach ($productIds as $index => $productId) { if (!$productId) continue; $systemStock = floatval($systemStocks[$index] ?? 0); $physicalStock = floatval($physicalStocks[$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) ); } } // 8. Create Opname master record with approved status $opname = Opname::create([ 'dealer_id' => $dealerId, 'opname_date' => $opnameDate, 'user_id' => $userId, 'note' => $note, 'status' => OpnameStatus::APPROVED, // Set status langsung approved 'approved_by' => $userId, // Set current user sebagai approver 'approved_at' => now() // Set waktu approval ]); // 9. Create OpnameDetails and update stock - only for valid entries $details = []; $processedCount = 0; foreach ($productIds as $index => $productId) { if (!$productId) continue; // Skip only if physical stock is truly not provided (empty string or null) // Accept 0 as valid input if (!isset($physicalStocks[$index]) || $physicalStocks[$index] === '' || $physicalStocks[$index] === null) { continue; } // Validate that physical stock is numeric (including 0) if (!is_numeric($physicalStocks[$index])) { continue; } $systemStock = floatval($systemStocks[$index] ?? 0); $physicalStock = floatval($physicalStocks[$index]); $difference = $physicalStock - $systemStock; $processedCount++; // Get item note (only for regular opname form) $itemNote = null; if (!$isTransactionForm) { $itemNote = $request->input("item_notes.{$index}"); } // Create opname detail $details[] = [ 'opname_id' => $opname->id, 'product_id' => $productId, 'system_stock' => $systemStock, 'physical_stock' => $physicalStock, 'difference' => $difference, 'note' => $itemNote, 'created_at' => now(), 'updated_at' => now() ]; // Update stock langsung karena auto approve $stock = Stock::firstOrCreate( [ 'product_id' => $productId, 'dealer_id' => $dealerId ], ['quantity' => 0] ); // Update stock dengan physical stock $stock->updateStock( $physicalStock, $opname, "Stock adjustment from auto-approved opname #{$opname->id}" ); } // Validate we have at least one detail to insert if (empty($details)) { throw new \Exception('Tidak ada data stock fisik yang valid untuk diproses.'); } // Bulk insert untuk performa lebih baik OpnameDetail::insert($details); // 10. Log aktivitas dengan detail produk yang diproses $processedProducts = collect($details)->map(function($detail) { return [ 'product_id' => $detail['product_id'], 'system_stock' => $detail['system_stock'], 'physical_stock' => $detail['physical_stock'], 'difference' => $detail['difference'] ]; }); Log::info('Opname created and auto-approved', [ 'opname_id' => $opname->id, 'dealer_id' => $opname->dealer_id, 'user_id' => $userId, 'approver_id' => $userId, 'product_count' => count($details), 'processed_count' => $processedCount, 'form_type' => $isTransactionForm ? 'transaction' : 'regular', 'opname_date' => $opnameDate->format('Y-m-d'), 'processed_products' => $processedProducts->toArray() ]); DB::commit(); if ($isTransactionForm) { // Redirect back to transaction page with success message and tab indicator return redirect() ->route('transaction') ->with('success', "Opname berhasil disimpan dan disetujui. {$processedCount} produk telah diproses.") ->with('active_tab', 'opname'); } else { // Redirect to opname index for regular form return redirect() ->route('opnames.index') ->with('success', "Opname berhasil disimpan dan disetujui. {$processedCount} produk telah diproses."); } } catch (\Illuminate\Validation\ValidationException $e) { DB::rollBack(); Log::error('Validation error in OpnamesController@store', [ 'errors' => $e->errors(), 'input' => $request->all() ]); if ($isTransactionForm) { return redirect() ->route('transaction') ->withErrors($e->validator) ->withInput() ->with('error', 'Terjadi kesalahan validasi. Periksa kembali data yang dimasukkan.') ->with('active_tab', 'opname'); } else { return back()->withErrors($e->validator)->withInput(); } } catch (\Exception $e) { DB::rollBack(); Log::error('Error in OpnamesController@store: ' . $e->getMessage()); Log::error($e->getTraceAsString()); Log::error('Request data:', $request->all()); $errorMessage = $e->getMessage(); if ($isTransactionForm) { return redirect() ->route('transaction') ->with('error', $errorMessage) ->withInput() ->with('active_tab', 'opname'); } else { return back() ->with('error', $errorMessage) ->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); } } }