first();
abort_if(!Gate::allows('view', $menu), 403);
$dealers = Dealer::all();
if($request->ajax()){
$data = Opname::query()
->with(['user','dealer', 'details.product'])
->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('stock_info', function ($row) {
// Use eager loaded details
$details = $row->details;
if ($details->isEmpty()) {
return 'Tidak ada data';
}
$totalProducts = $details->count();
$matchingProducts = $details->where('difference', 0)->count();
$differentProducts = $totalProducts - $matchingProducts;
$info = [];
if ($matchingProducts > 0) {
$info[] = " {$matchingProducts} sesuai";
}
if ($differentProducts > 0) {
// Get more details about differences
$positiveDiff = $details->where('difference', '>', 0)->count();
$negativeDiff = $details->where('difference', '<', 0)->count();
$diffInfo = [];
if ($positiveDiff > 0) {
$diffInfo[] = "+{$positiveDiff}";
}
if ($negativeDiff > 0) {
$diffInfo[] = "-{$negativeDiff}";
}
$diffText = implode(', ', $diffInfo);
$info[] = " {$differentProducts} selisih ({$diffText})";
}
// Add total products info
$info[] = "(Total: {$totalProducts} produk)";
return '
' . implode('
', $info) . '
';
})
->addColumn('action', function ($row) use ($menu) {
$btn = '';
return $btn;
})
->rawColumns(['action', 'status', 'stock_info'])
->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) {
// Simplified 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.*' => 'nullable|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. Simplified validation - all products are valid, set defaults for empty physical stocks
$validProductIds = array_filter($productIds);
if (empty($validProductIds) || count($validProductIds) === 0) {
throw new \Exception('Minimal harus ada satu produk untuk opname.');
}
// 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;
// Set default value to 0 if physical stock is empty or invalid
$physicalStockValue = $physicalStocks[$index] ?? null;
if ($physicalStockValue === '' || $physicalStockValue === null || !is_numeric($physicalStockValue)) {
$physicalStockValue = 0;
}
$systemStock = floatval($systemStocks[$index] ?? 0);
$physicalStock = floatval($physicalStockValue);
$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 produk 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. {$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. {$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);
}
}
public function print($id)
{
try {
$opname = Opname::with(['details.product.category', 'user', 'dealer'])
->findOrFail($id);
return view('warehouse_management.opnames.print', compact('opname'));
} catch (\Exception $e) {
Log::error('Error printing opname: ' . $e->getMessage());
return back()->with('error', 'Gagal membuka halaman print opname.');
}
}
}