user()->role !== 'admin') { return $this->karyawanIndex($request); } $branches = \App\Models\Branch::all(); $selectedBranch = $request->input('branch', 'Total Gabungan'); $query = Product::with(['stocks', 'recipes.rawMaterial']); if (auth()->user()->role !== 'admin') { $query->where('branch', auth()->user()->branch ?? 'Cabang 1'); } elseif ($selectedBranch && $selectedBranch !== 'Total Gabungan') { $query->where('branch', $selectedBranch); } if ($request->filled('search')) { $query->where(function($q) use ($request) { $q->where('name', 'like', '%' . $request->search . '%') ->orWhere('category', 'like', '%' . $request->search . '%'); }); } $products = $query->get(); $selectedRawMaterialBranch = $request->input('raw_material_branch', 'Total Gabungan'); $rawMaterialsQuery = \App\Models\RawMaterial::query(); if ($selectedRawMaterialBranch && $selectedRawMaterialBranch !== 'Total Gabungan') { $rawMaterialsQuery->where('branch', $selectedRawMaterialBranch); } $rawMaterials = $rawMaterialsQuery->get(); return view('produk', compact('products', 'branches', 'selectedBranch', 'rawMaterials', 'selectedRawMaterialBranch')); } public function store(Request $request) { $request->validate([ 'name' => 'required|string|max:255', 'category' => 'required|in:makanan,minuman,snack', 'purchase_price' => 'required|integer', 'selling_price' => 'required|integer', 'image' => 'nullable|image|mimes:jpeg,png,jpg,gif,svg|max:2048', ]); $data = $request->all(); $branch = auth()->user()->branch ?? 'Cabang 1'; $data['branch'] = $branch; // Unique name check per branch $exists = Product::whereRaw('LOWER(name) = ?', [strtolower($request->name)]) ->where('branch', $branch) ->exists(); if ($exists) { return back()->withInput()->withErrors(['duplicate_error' => 'Produk dengan nama "' . $request->name . '" sudah ada di ' . $branch . '.']); } if ($request->hasFile('image')) { $file = $request->file('image'); $filename = time() . '_' . preg_replace('/[^a-zA-Z0-9._-]/', '', $file->getClientOriginalName()); $destination = public_path('images/produk'); if (!file_exists($destination)) { mkdir($destination, 0775, true); } $file->move($destination, $filename); $data['image'] = '/images/produk/' . $filename; } $product = Product::create($data); // Save Recipes if any if ($request->has('recipes') && is_array($request->recipes)) { foreach ($request->recipes as $recipeData) { if (isset($recipeData['raw_material_id']) && isset($recipeData['quantity_needed']) && $recipeData['quantity_needed'] !== '') { // Parse quantity string to allow '300 ml', '0.5', '300' $parsed = $this->parseQuantity($recipeData['quantity_needed']); $rm = \App\Models\RawMaterial::find($recipeData['raw_material_id']); $quantityToStore = $parsed['value']; // If unit provided and differs from raw material unit, try to convert if ($parsed['unit'] && $rm) { $quantityToStore = $this->convertRecipeToRawMaterialUnit($parsed['value'], $parsed['unit'], $rm->unit); } \App\Models\ProductRecipe::create([ 'product_id' => $product->id, 'raw_material_id' => $recipeData['raw_material_id'], 'quantity_needed' => $quantityToStore, ]); } } // Recalculate stock based on raw materials $this->recalculateProductsFromRawMaterials($product->id, $branch); } // Create initial stock for THIS branch only \App\Models\Stock::create([ 'product_id' => $product->id, 'branch' => $branch, 'quantity' => 0, 'unit' => 'Pcs', ]); return back()->with('success', 'Produk Master berhasil ditambahkan!'); } public function update(Request $request, $id) { $request->validate([ 'name' => 'required|string|max:255', 'category' => 'required|in:makanan,minuman,snack', 'purchase_price' => 'required|integer', 'selling_price' => 'required|integer', 'image' => 'nullable|image|mimes:jpeg,png,jpg,gif,svg|max:2048', ]); $product = Product::findOrFail($id); $branch = auth()->user()->branch ?? 'Cabang 1'; // Only allow update if product belongs to user's branch if ($product->branch !== $branch) { return back()->with('error', 'Anda tidak memiliki akses untuk mengubah produk dari cabang lain.'); } // Unique name check for update per branch $exists = Product::whereRaw('LOWER(name) = ?', [strtolower($request->name)]) ->where('branch', $branch) ->where('id', '!=', $id) ->exists(); if ($exists) { return back()->withInput()->withErrors(['duplicate_error' => 'Produk dengan nama "' . $request->name . '" sudah ada di ' . $branch . '.']); } $data = $request->only(['name', 'category', 'purchase_price', 'selling_price']); if ($request->hasFile('image')) { // Delete old image if it exists in our public/images/produk directory if ($product->image && str_contains($product->image, '/images/produk/')) { $oldFile = public_path($product->image); if (file_exists($oldFile)) { unlink($oldFile); } } // Also handle old images stored via storage symlink if ($product->image && str_contains($product->image, '/storage/produk/')) { $oldPath = str_replace('/storage/', '', $product->image); Storage::disk('public')->delete($oldPath); } $file = $request->file('image'); $filename = time() . '_' . preg_replace('/[^a-zA-Z0-9._-]/', '', $file->getClientOriginalName()); $destination = public_path('images/produk'); if (!file_exists($destination)) { mkdir($destination, 0775, true); } $file->move($destination, $filename); $data['image'] = '/images/produk/' . $filename; } $product->update($data); // Update Recipes \App\Models\ProductRecipe::where('product_id', $product->id)->delete(); if ($request->has('recipes') && is_array($request->recipes)) { foreach ($request->recipes as $recipeData) { if (isset($recipeData['raw_material_id']) && isset($recipeData['quantity_needed']) && $recipeData['quantity_needed'] !== '') { $parsed = $this->parseQuantity($recipeData['quantity_needed']); $rm = \App\Models\RawMaterial::find($recipeData['raw_material_id']); $quantityToStore = $parsed['value']; if ($parsed['unit'] && $rm) { $quantityToStore = $this->convertRecipeToRawMaterialUnit($parsed['value'], $parsed['unit'], $rm->unit); } \App\Models\ProductRecipe::create([ 'product_id' => $product->id, 'raw_material_id' => $recipeData['raw_material_id'], 'quantity_needed' => $quantityToStore, ]); } } // Recalculate stock based on raw materials $this->recalculateProductsFromRawMaterials($product->id, $branch); } return back()->with('success', 'Produk berhasil diperbarui!'); } public function destroy($id) { $product = Product::findOrFail($id); $branch = auth()->user()->branch ?? 'Cabang 1'; // Only allow deletion if product belongs to user's branch if ($product->branch !== $branch) { return back()->with('error', 'Anda tidak memiliki akses untuk menghapus produk dari cabang lain.'); } $product->delete(); return back()->with('success', 'Produk berhasil dihapus!'); } // Karyawan: lihat produk dengan stok per cabang dan catat penjualan sederhana public function karyawanIndex(Request $request) { $branch = auth()->user()->branch ?? 'Cabang 1'; $query = Product::with(['stocks' => function($q) use ($branch) { $q->where('branch', $branch); }, 'recipes.rawMaterial'])->where('branch', $branch); if ($request->filled('search')) { $query->where('name', 'like', '%' . $request->search . '%') ->orWhere('category', 'like', '%' . $request->search . '%'); } $products = $query->get(); $rawMaterials = \App\Models\RawMaterial::where('branch', $branch)->get(); return view('produk_karyawan', compact('products', 'branch', 'rawMaterials')); } public function sell(Request $request, $id) { $request->validate([ 'quantity' => 'required|integer|min:1' ]); $branch = auth()->user()->branch ?? 'Cabang 1'; $qty = (int) $request->input('quantity'); $product = Product::with('recipes.rawMaterial')->findOrFail($id); if ($product->recipes->count() > 0) { // Check raw material stock first foreach ($product->recipes as $recipe) { $rm = $recipe->rawMaterial; if (!$rm) continue; $needed = $recipe->quantity_needed * $qty; if ($rm->stock_quantity < $needed) { return back()->withErrors(['stock' => 'Bahan baku tidak cukup untuk membuat ' . $qty . ' porsi.']); } } // Deduct raw materials foreach ($product->recipes as $recipe) { $rm = $recipe->rawMaterial; if ($rm) { $rm->stock_quantity -= ($recipe->quantity_needed * $qty); $rm->save(); // Optional: recalculate other products that depend on this raw material $this->recalculateProductsFromRawMaterial($rm->id, $branch); } } } else { // Deduct from regular stock $stock = Stock::where('product_id', $id)->where('branch', $branch)->first(); if (!$stock) { return back()->withErrors(['stock' => 'Stok untuk produk ini tidak ditemukan pada cabang Anda.']); } if ($stock->quantity < $qty) { return back()->withErrors(['stock' => 'Stok tidak cukup. Tersedia: ' . $stock->quantity]); } // Kurangi stok $stock->quantity = $stock->quantity - $qty; $stock->save(); } // Catat penjualan sederhana Sale::create([ 'date' => now()->toDateString(), 'branch' => $branch, 'product_id' => $product->id, 'item' => $product->name, 'quantity' => $qty, 'total_price' => ($product->selling_price ?? 0) * $qty, ]); return back()->with('success', 'Penjualan berhasil dicatat dan stok diperbarui.'); } 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(); } } // Parse quantity strings used in recipe inputs (e.g. '300 ml', '0.5', '300') private function parseQuantity($quantityStr) { $quantityStr = trim((string)$quantityStr); if (preg_match('/^([\d.,]+)\s*(.*)$/', $quantityStr, $m)) { $value = floatval(str_replace(',', '.', $m[1])); $unit = strtolower(trim($m[2])); return ['value' => $value, 'unit' => $unit]; } return ['value' => floatval($quantityStr), 'unit' => '']; } // Convert a recipe quantity with unit into the raw material's unit if possible private function convertRecipeToRawMaterialUnit($value, $fromUnit, $toUnit) { $from = strtolower(trim($fromUnit)); $to = strtolower(trim($toUnit)); // If same unit or no fromUnit, return value as-is if (!$from || $from === $to) return $value; // Basic conversions: liters <-> ml, kilogram <-> gram $mapping = [ 'l' => ['ml' => 1000, 'liter' => 1000], 'liter' => ['ml' => 1000, 'l' => 1], 'ml' => ['l' => 1/1000, 'liter' => 1/1000], 'kg' => ['g' => 1000, 'kilogram' => 1000], 'kilogram' => ['g' => 1000, 'kg' => 1], 'g' => ['kg' => 1/1000, 'gram' => 1/1000], 'gram' => ['kg' => 1/1000, 'g' => 1], ]; // Normalize tokens $aliases = [ 'grams' => 'g', 'gram' => 'g', 'gr' => 'g', 'milliliter' => 'ml', 'mililiter' => 'ml', 'litre' => 'l', 'liters' => 'l', 'liter' => 'l', 'kilograms' => 'kg', 'kilogram' => 'kg', 'pcs' => 'pcs', 'piece' => 'pcs', 'pc' => 'pcs', 'botol' => 'botol' ]; $fromNorm = $aliases[$from] ?? $from; $toNorm = $aliases[$to] ?? $to; // If one is pcs and other is numeric unit, do not convert automatically if (($fromNorm === 'pcs' && $toNorm !== 'pcs') || ($toNorm === 'pcs' && $fromNorm !== 'pcs')) { return $value; } if (isset($mapping[$fromNorm]) && isset($mapping[$fromNorm][$toNorm])) { return $value * $mapping[$fromNorm][$toNorm]; } // If direct mapping not found, try inverse if (isset($mapping[$toNorm]) && isset($mapping[$toNorm][$fromNorm])) { return $value / $mapping[$toNorm][$fromNorm]; } return $value; } private function recalculateProductsFromRawMaterial($rawMaterialId, $branch) { $recipes = \App\Models\ProductRecipe::where('raw_material_id', $rawMaterialId) ->whereHas('product', function($q) use ($branch) { $q->where('branch', $branch); })->get(); foreach ($recipes as $recipe) { $product = clone $recipe->product; $minStock = null; foreach ($product->recipes as $prodRecipe) { $rm = $prodRecipe->rawMaterial; $possibleAmount = floor($rm->stock_quantity / $prodRecipe->quantity_needed); if ($minStock === null || $possibleAmount < $minStock) { $minStock = $possibleAmount; } } if ($minStock !== null) { $stock = \App\Models\Stock::firstOrCreate( ['product_id' => $product->id, 'branch' => $branch], ['quantity' => 0, 'unit' => 'Pcs'] ); $stock->quantity = $minStock; $stock->save(); } } } public function updateStock(Request $request, $id) { $request->validate([ 'quantity' => 'required|integer' ]); $branch = auth()->user()->branch ?? 'Cabang 1'; $stock = Stock::where('product_id', $id)->where('branch', $branch)->first(); if (!$stock) { return back()->withErrors(['stock' => 'Stok untuk produk ini tidak ditemukan pada cabang Anda.']); } $stock->quantity += (int)$request->input('quantity'); if ($stock->quantity < 0) $stock->quantity = 0; $stock->save(); return back()->with('success', 'Stok berhasil diperbarui.'); } }