orderBy('date', 'desc'); if ($request->filled('year')) { $query->whereYear('date', $request->year); } if ($request->filled('month')) { $query->whereMonth('date', $request->month); } if ($request->filled('day')) { $query->whereDay('date', $request->day); } if ($request->filled('search')) { $query->where('item', 'like', '%' . $request->search . '%'); } // Apply "today" filter by default only when the page is first loaded // (i.e. there are no query parameters). If the user submitted the filter // form with "Semua Tahun" (empty values), we treat that as an explicit // request and do not force today's data. if (count($request->query()) === 0) { $query->whereDate('date', \Carbon\Carbon::today()); } if ($user->role === 'admin') { $products = \App\Models\Product::all(); $sales = $query->get(); // We no longer need salesByBranch as we use a single consolidated view return view('laporan_penjualan', compact('sales', 'products', 'branches')); } else { $products = \App\Models\Product::all(); $sales = $query->where('branch', $user->branch)->get(); return view('laporan_penjualan', compact('sales', 'products', 'branches')); } } public function create() { $user = Auth::user(); if ($user->role === 'admin') { return redirect('/laporan/penjualan')->with('error', 'Fitur tambah penjualan hanya tersedia untuk karyawan.'); } $branches = \App\Models\Branch::all(); $branch = $user->branch ?? 'Cabang 1'; $products = \App\Models\Product::where('branch', $branch)->get(); return view('transaksi.penjualan', compact('branches', 'products')); } public function exportPdf(Request $request) { $user = Auth::user(); if ($user->role !== 'admin') { abort(403); } $query = Sale::with('product')->orderBy('date', 'desc'); if ($request->filled('year')) { $query->whereYear('date', $request->year); } if ($request->filled('month')) { $query->whereMonth('date', $request->month); } if ($request->filled('day')) { $query->whereDay('date', $request->day); } if ($request->filled('search')) { $query->where('item', 'like', '%' . $request->search . '%'); } // Apply "today" filter by default only when the page is first loaded // (i.e. there are no query parameters). If the admin submits the filter // form with empty values intentionally, return the full dataset. if (count($request->query()) === 0) { $query->whereDate('date', \Carbon\Carbon::today()); } $sales = $query->get(); $branches = \App\Models\Branch::all(); $pdf = \Barryvdh\DomPDF\Facade\Pdf::loadView('reports.sale_pdf', compact('sales', 'branches')); return $pdf->download('laporan_penjualan.pdf'); } public function store(Request $request) { $request->validate([ 'date' => 'required|date', 'product_id' => 'required|exists:products,id', 'quantity' => 'required|integer|min:1', 'total_price' => 'required|numeric|min:0', ]); try { \Illuminate\Support\Facades\DB::transaction(function () use ($request) { $product = \App\Models\Product::findOrFail($request->product_id); $branch = Auth::user()->branch ?? 'Cabang 1'; if (Auth::user()->role === 'admin' && $request->has('branch')) { $branch = $request->branch; } // Bersihkan format angka (hapus titik pemisah ribuan jika ada) $cleanPrice = str_replace('.', '', $request->total_price); // First, adjust stocks: either raw materials (if product has recipes) // or product stock per branch $product = \App\Models\Product::with('recipes.rawMaterial')->findOrFail($request->product_id); if ($product->recipes->count() > 0) { // Check and deduct raw materials foreach ($product->recipes as $recipe) { $rm = $recipe->rawMaterial; if (!$rm) continue; $needed = $recipe->quantity_needed * $request->quantity; if ($rm->stock_quantity < $needed) { throw new \Exception('Bahan baku tidak cukup untuk membuat ' . $request->quantity . ' porsi.'); } } foreach ($product->recipes as $recipe) { $rm = $recipe->rawMaterial; if ($rm) { $needed = $recipe->quantity_needed * $request->quantity; $rm->stock_quantity -= $needed; if ($rm->stock_quantity < 0) $rm->stock_quantity = 0; $rm->save(); } } // Recalculate finished product stock once after all raw materials are updated $this->recalculateProductsFromRawMaterials($product->id, $branch); } else { // Deduct from regular stock $stock = \App\Models\Stock::where('product_id', $request->product_id)->where('branch', $branch)->first(); if (!$stock) { throw new \Exception('Stok untuk produk ini tidak ditemukan pada cabang Anda.'); } if ($stock->quantity < $request->quantity) { throw new \Exception('Stok tidak cukup. Tersedia: ' . $stock->quantity); } $stock->quantity = $stock->quantity - $request->quantity; if ($stock->quantity < 0) $stock->quantity = 0; $stock->save(); } // Now create sale record $sale = Sale::create([ 'date' => $request->date, 'branch' => $branch, 'product_id' => $request->product_id, 'item' => $product->name, 'quantity' => $request->quantity, 'total_price' => (int) $cleanPrice, ]); // Create Journal Entry $kasAccount = $this->ensureAccount('101', 'Kas', 'Asset'); $penjualanAccount = $this->ensureAccount('401', 'Penjualan', 'Revenue'); $hppAccount = $this->ensureAccount('501', 'Harga Pokok Penjualan', 'Expense'); $persediaanAccount = $this->ensureAccount('102', 'Persediaan Barang Dagang', 'Asset'); $journal = JournalEntry::create([ 'date' => $sale->date, 'description' => 'Penjualan ' . $sale->item, 'reference_type' => 'Sale', 'reference_id' => $sale->id, 'branch' => $branch, ]); // Debit Kas, Kredit Penjualan JournalDetail::create(['journal_entry_id' => $journal->id, 'account_id' => $kasAccount->id, 'debit' => $sale->total_price, 'credit' => 0]); JournalDetail::create(['journal_entry_id' => $journal->id, 'account_id' => $penjualanAccount->id, 'debit' => 0, 'credit' => $sale->total_price]); // Debit HPP, Kredit Persediaan $hppTotal = $sale->quantity * $product->purchase_price; JournalDetail::create(['journal_entry_id' => $journal->id, 'account_id' => $hppAccount->id, 'debit' => $hppTotal, 'credit' => 0]); JournalDetail::create(['journal_entry_id' => $journal->id, 'account_id' => $persediaanAccount->id, 'debit' => 0, 'credit' => $hppTotal]); }); return back()->with('success', 'Transaksi penjualan berhasil ditambahkan!'); } catch (\Exception $e) { \Illuminate\Support\Facades\Log::error('Sale store error: ' . $e->getMessage()); return back()->withInput()->withErrors(['db_error' => 'Gagal menyimpan transaksi: ' . $e->getMessage()]); } } private function ensureAccount($code, $name, $type) { return Account::updateOrCreate( ['code' => $code], ['name' => $name, 'type' => $type] ); } public function update(Request $request, $id) { $request->validate([ 'date' => 'required|date', 'product_id' => 'required|exists:products,id', 'quantity' => 'required|integer|min:1', 'total_price' => 'required|numeric|min:0', ]); try { \Illuminate\Support\Facades\DB::transaction(function () use ($request, $id) { $sale = Sale::findOrFail($id); $product = \App\Models\Product::findOrFail($request->product_id); // Bersihkan format angka (hapus titik pemisah ribuan jika ada) $cleanPrice = str_replace('.', '', $request->total_price); $data = $request->all(); $data['item'] = $product->name; $data['total_price'] = (int) $cleanPrice; if (Auth::user()->role === 'admin' && $request->has('branch')) { $data['branch'] = $request->branch; } $sale->update($data); // Recreate Journal Entry JournalEntry::where('reference_type', 'Sale')->where('reference_id', $sale->id)->delete(); $kasAccount = Account::where('code', '101')->first(); $penjualanAccount = Account::where('code', '401')->first(); $hppAccount = Account::where('code', '501')->first(); $persediaanAccount = Account::where('code', '102')->first(); if ($kasAccount && $penjualanAccount && $hppAccount && $persediaanAccount) { $journal = JournalEntry::create([ 'date' => $sale->date, 'description' => 'Penjualan ' . $sale->item, 'reference_type' => 'Sale', 'reference_id' => $sale->id, 'branch' => $sale->branch, ]); JournalDetail::create(['journal_entry_id' => $journal->id, 'account_id' => $kasAccount->id, 'debit' => $sale->total_price, 'credit' => 0]); JournalDetail::create(['journal_entry_id' => $journal->id, 'account_id' => $penjualanAccount->id, 'debit' => 0, 'credit' => $sale->total_price]); $hppTotal = $sale->quantity * $product->purchase_price; JournalDetail::create(['journal_entry_id' => $journal->id, 'account_id' => $hppAccount->id, 'debit' => $hppTotal, 'credit' => 0]); JournalDetail::create(['journal_entry_id' => $journal->id, 'account_id' => $persediaanAccount->id, 'debit' => 0, 'credit' => $hppTotal]); } }); return back()->with('success', 'Transaksi penjualan berhasil diperbarui!'); } catch (\Exception $e) { \Illuminate\Support\Facades\Log::error('Sale update error: ' . $e->getMessage()); return back()->withInput()->withErrors(['db_error' => 'Gagal memperbarui transaksi: ' . $e->getMessage()]); } } public function destroy($id) { $sale = Sale::findOrFail($id); JournalEntry::where('reference_type', 'Sale')->where('reference_id', $sale->id)->delete(); $sale->delete(); return back()->with('success', 'Transaksi penjualan berhasil dihapus!'); } private function recalculateProductsFromRawMaterials($productId, $branch) { $product = \App\Models\Product::with('recipes.rawMaterial')->find($productId); if (!$product || $product->recipes->isEmpty()) { return; } $minStock = null; foreach ($product->recipes as $recipe) { $rm = $recipe->rawMaterial; if (!$rm || $recipe->quantity_needed <= 0) { continue; } $possibleAmount = floor($rm->stock_quantity / $recipe->quantity_needed); if ($minStock === null || $possibleAmount < $minStock) { $minStock = $possibleAmount; } } if ($minStock !== null && $minStock >= 0) { $stock = \App\Models\Stock::firstOrCreate( ['product_id' => $productId, 'branch' => $branch], ['quantity' => 0, 'unit' => 'Pcs'] ); $stock->quantity = $minStock; $stock->save(); } } private function recalculateProductsFromRawMaterial($rawMaterialId, $branch) { $recipes = \App\Models\ProductRecipe::where('raw_material_id', $rawMaterialId) ->with(['product.recipes.rawMaterial']) ->get(); foreach ($recipes as $recipe) { $product = $recipe->product; if (!$product) continue; $minStock = null; foreach ($product->recipes as $prodRecipe) { $rm = $prodRecipe->rawMaterial; if (!$rm || $prodRecipe->quantity_needed <= 0) continue; $rmStock = $rm->stock_quantity; $needed = $prodRecipe->quantity_needed; $possibleAmount = floor($rmStock / $needed); if ($minStock === null || $possibleAmount < $minStock) { $minStock = $possibleAmount; } } if ($minStock !== null && $minStock >= 0) { $allBranches = \App\Models\Branch::all(); foreach ($allBranches as $b) { $stock = \App\Models\Stock::firstOrCreate( ['product_id' => $product->id, 'branch' => $b->name], ['quantity' => 0, 'unit' => 'Pcs'] ); $stock->quantity = $minStock; $stock->save(); } } } } }