akuntansi-sia/app/Http/Controllers/ProductController.php

436 lines
17 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Models\Product;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use App\Models\Stock;
use App\Models\Sale;
class ProductController extends Controller
{
public function index(Request $request)
{
// If user is karyawan, use karyawanIndex logic
if (auth()->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.');
}
}