middleware('check.permission:products.read')->only(['index', 'show', 'expiredProduct']); $this->middleware('check.permission:products.create')->only(['create', 'store']); $this->middleware('check.permission:products.update')->only(['edit', 'update', 'CreateStock']); $this->middleware('check.permission:products.delete')->only(['destroy', 'deleteAll']); } public function index(Request $request) { $user = auth()->user(); $search = $request->input('search'); $products = Product::query() ->where('business_id', $user->business_id) ->with([ 'stocks', 'unit:id,unitName', 'brand:id,brandName', 'category:id,categoryName', 'warehouse:id,name', 'rack:id,name', 'shelf:id,name', 'combo_products.stock.product:id,productName' ]) ->withSum('stocks as total_stock', 'productStock') ->where(function ($query) { $query->where('product_type', '!=', 'combo') ->orWhere(function ($q) { $q->where('product_type', 'combo') ->whereHas('combo_products'); }); }) ->when($search, function ($q) use ($search) { $q->where(function ($q) use ($search) { $q->where('productName', 'like', "%{$search}%") ->orWhere('productCode', 'like', "%{$search}%") ->orWhere('productPurchasePrice', 'like', "%{$search}%") ->orWhere('productSalePrice', 'like', "%{$search}%") ->orWhere('product_type', 'like', "%{$search}%") ->orWhereHas('category', fn($q) => $q->where('categoryName', 'like', "%{$search}%")) ->orWhereHas('brand', fn($q) => $q->where('brandName', 'like', "%{$search}%")) ->orWhereHas('unit', fn($q) => $q->where('unitName', 'like', "%{$search}%")); }); }) ->latest() ->paginate($request->per_page ?? 20) ->appends($request->query()); $products->getCollection()->transform(function ($product) { if ($product->product_type === 'combo') { $product->total_stock = $product->combo_products->sum( fn($combo) => $combo->stock?->productStock ?? 0 ); $product->total_cost = $product->combo_products->sum( fn($combo) => ($combo->quantity ?? 0) * ($combo->purchase_price ?? 0) ); $product->combo_items = $product->combo_products->map(function ($combo) { return [ 'name' => $combo->stock?->product?->productName ?? 'N/A', 'quantity' => $combo->quantity ?? 0, 'purchase_price' => currency_format( ($combo->purchase_price ?? 0) * ($combo->quantity ?? 0), currency: business_currency() ), 'stock' => $combo->stock?->productStock ?? 0, ]; }); } return $product; }); if ($request->ajax()) { return response()->json([ 'data' => view('business::products.datas', compact('products'))->render() ]); } return view('business::products.index', compact('products')); } public function create() { $business_id = auth()->user()->business_id; $categories = Category::where('business_id', $business_id)->whereStatus(1)->latest()->get(); $brands = Brand::where('business_id', $business_id)->whereStatus(1)->latest()->get(); $units = Unit::where('business_id', $business_id)->whereStatus(1)->latest()->get(); $product_id = (Product::where('business_id', $business_id)->count() ?? 0) + 1; $vats = Vat::where('business_id', $business_id)->latest()->get(); $code = str_pad($product_id, 4, '0', STR_PAD_LEFT); $product_models = ProductModel::where('business_id', $business_id)->latest()->get(); $warehouses = Warehouse::where('business_id', $business_id)->latest()->get(); $variations = Variation::where('business_id', auth()->user()->business_id)->where('status', 1)->get(); $racks = Rack::where('business_id', $business_id)->latest()->get(); $shelves = Shelf::where('business_id', $business_id)->latest()->get(); $profit_option = Option::where('key', 'business-settings') ->where('value', 'LIKE', '%"business_id":%' . $business_id . '%') ->get() ->firstWhere('value.business_id', $business_id)['product_profit_option'] ?? ''; return view('business::products.create', compact('categories', 'brands', 'units', 'code', 'vats', 'product_models', 'warehouses', 'racks', 'shelves', 'profit_option', 'variations')); } /** * Store a newly created resource in storage. */ public function store(Request $request) { $business_id = auth()->user()->business_id; $request->validate([ 'vat_id' => 'nullable|exists:vats,id', 'unit_id' => 'nullable|exists:units,id', 'brand_id' => 'nullable|exists:brands,id', 'category_id' => 'nullable|exists:categories,id', 'model_id' => 'nullable|exists:product_models,id', 'vat_type' => 'nullable|in:inclusive,exclusive', 'productName' => 'required|string|max:255', 'productPicture' => 'nullable|image|mimes:jpg,png,jpeg,svg', 'productCode' => [ 'nullable', Rule::unique('products')->where(function ($query) use ($business_id) { return $query->where('business_id', $business_id); }), ], 'alert_qty' => 'nullable|numeric|min:0', 'size' => 'nullable|string|max:255', 'type' => 'nullable|string|max:255', 'color' => 'nullable|string|max:255', 'weight' => 'nullable|string|max:255', 'capacity' => 'nullable|string|max:255', 'productManufacturer' => 'nullable|string|max:255', 'product_type' => 'required|in:single,variant,combo', 'variation_ids' => 'nullable|array|exists:variations,id', 'stocks.*.warehouse_id' => 'nullable|exists:warehouses,id', 'stocks.*.productStock' => 'nullable|numeric|min:0|max:99999999.99', 'stocks.*.exclusive_price' => 'nullable|numeric|min:0|max:99999999.99', 'stocks.*.inclusive_price' => 'nullable|numeric|min:0|max:99999999.99', 'stocks.*.profit_percent' => 'nullable|numeric|max:99999999.99', 'stocks.*.productSalePrice' => 'nullable|numeric|min:0|max:99999999.99', 'stocks.*.productWholeSalePrice' => 'nullable|numeric|min:0|max:99999999.99', 'stocks.*.productDealerPrice' => 'nullable|numeric|min:0|max:99999999.99', 'stocks.*.mfg_date' => 'nullable|date', 'stocks.*.expire_date' => 'nullable|date|after_or_equal:stocks.*.mfg_date', 'stocks' => 'nullable|array', 'stocks.*.batch_no' => [ 'nullable', function ($attribute, $value, $fail) use ($request) { $batchNos = collect($request->stocks)->pluck('batch_no')->filter()->toArray(); if (count($batchNos) !== count(array_unique($batchNos))) { $fail('Duplicate batch number found in the request.'); } }, ], // Combo validation 'combo_products' => 'nullable|array', 'combo_products.*.stock_id' => [ 'required_if:product_type,combo', Rule::exists('stocks', 'id')->where('business_id', $business_id), ], 'combo_products.*.quantity' => 'required_if:product_type,combo|numeric|min:1', ]); DB::beginTransaction(); try { // vat calculation $vat = Vat::find($request->vat_id); $vat_rate = $vat->rate ?? 0; // Create the product $product = Product::create($request->only('productName', 'unit_id', 'brand_id', 'vat_id', 'vat_type', 'category_id', 'productCode', 'product_type', 'rack_id', 'shelf_id', 'model_id', 'variation_ids', 'warranty_guarantee_info') + [ 'business_id' => $business_id, 'alert_qty' => $request->alert_qty ?? 0, 'is_displayed_in_pos' => $request->has('is_displayed_in_pos') ? 1 : 0, 'profit_percent' => $request->product_type == 'combo' ? $request->profit_percent ?? 0 : 0, 'productSalePrice' => $request->product_type == 'combo' ? $request->productSalePrice ?? 0 : 0, 'productPicture' => $request->productPicture ? $this->upload($request, 'productPicture') : NULL, ]); // Single or Variant Product if (in_array($request->product_type, ['single', 'variant']) && !empty($request->stocks)) { $stockData = []; foreach ($request->stocks as $stock) { $base_price = $stock['exclusive_price'] ?? 0; $purchasePrice = $request->vat_type === 'inclusive' ? $base_price + ($base_price * $vat_rate / 100) : $base_price; $stockData[] = [ 'business_id' => $business_id, 'product_id' => $product->id, 'batch_no' => $stock['batch_no'] ?? null, 'warehouse_id' => $stock['warehouse_id'] ?? null, 'productStock' => $stock['productStock'] ?? 0, 'productPurchasePrice' => $purchasePrice, 'profit_percent' => $stock['profit_percent'] ?? 0, 'productSalePrice' => $stock['productSalePrice'] ?? 0, 'productWholeSalePrice' => $stock['productWholeSalePrice'] ?? 0, 'productDealerPrice' => $stock['productDealerPrice'] ?? 0, 'mfg_date' => $stock['mfg_date'] ?? null, 'expire_date' => $stock['expire_date'] ?? null, 'variation_data' => $stock['variation_data'] ?? null, 'variant_name' => $stock['variant_name'] ?? null, 'serial_numbers' => $request->has_serial ? json_encode($stock['serial_numbers'] ?? []) : null, 'branch_id' => auth()->user()->branch_id ?? auth()->user()->active_branch_id, 'created_at' => now(), 'updated_at' => now(), ]; } Stock::insert($stockData); } // Combo Product if ($request->product_type === 'combo' && !empty($request->combo_products)) { foreach ($request->combo_products as $item) { ComboProduct::create([ 'product_id' => $product->id, 'stock_id' => $item['stock_id'], 'quantity' => $item['quantity'], 'purchase_price' => $item['purchase_price'], ]); } } DB::commit(); return response()->json([ 'message' => __('Product saved successfully.'), 'redirect' => route('business.products.index') ]); } catch (\Exception $e) { DB::rollback(); return response()->json([ 'message' => $e->getMessage(), ], 406); } } public function edit($id) { $business_id = auth()->user()->business_id; $categories = Category::where('business_id', $business_id)->whereStatus(1)->latest()->get(); $brands = Brand::where('business_id', $business_id)->whereStatus(1)->latest()->get(); $units = Unit::where('business_id', $business_id)->whereStatus(1)->latest()->get(); $vats = Vat::where('business_id', $business_id)->latest()->get(); $product_models = ProductModel::where('business_id', $business_id)->latest()->get(); $warehouses = Warehouse::where('business_id', $business_id)->latest()->get(); $racks = Rack::where('business_id', $business_id)->latest()->get(); $shelves = Shelf::where('business_id', $business_id)->latest()->get(); $variations = Variation::where('business_id', auth()->user()->business_id)->where('status', 1)->get(); $profit_option = Option::where('key', 'business-settings') ->where('value', 'LIKE', '%"business_id":%' . $business_id . '%') ->get() ->firstWhere('value.business_id', $business_id)['product_profit_option'] ?? ''; $product = Product::with([ 'stocks' => function ($query) { $query->orderBy('variant_name', 'asc') ->orderBy('id', 'asc'); }, 'combo_products.stock:id,batch_no,product_id', 'combo_products.stock.product:id,productName,productCode,unit_id', 'combo_products.stock.product.unit:id,unitName' ]) ->where('business_id', $business_id) ->findOrFail($id); return view('business::products.edit', compact('categories', 'brands', 'units', 'product', 'vats', 'product_models', 'warehouses', 'racks', 'shelves', 'profit_option', 'variations')); } public function update(Request $request, $id) { $product = Product::findOrFail($id); $business_id = auth()->user()->business_id; if ($product->product_type != $request->product_type) { return response()->json([ 'message' => __('Product type can not be changed.'), ], 406); } $request->validate([ 'vat_id' => 'nullable|exists:vats,id', 'unit_id' => 'nullable|exists:units,id', 'brand_id' => 'nullable|exists:brands,id', 'category_id' => 'nullable|exists:categories,id', 'model_id' => 'nullable|exists:product_models,id', 'vat_type' => 'nullable|in:inclusive,exclusive', 'productName' => 'required|string|max:255', 'productPicture' => 'nullable|image|mimes:jpg,png,jpeg,svg', 'productCode' => [ 'nullable', Rule::unique('products', 'productCode')->ignore($product->id)->where(function ($query) use ($business_id) { return $query->where('business_id', $business_id); }), ], 'alert_qty' => 'nullable|numeric|min:0', 'size' => 'nullable|string|max:255', 'type' => 'nullable|string|max:255', 'color' => 'nullable|string|max:255', 'weight' => 'nullable|string|max:255', 'capacity' => 'nullable|string|max:255', 'productManufacturer' => 'nullable|string|max:255', 'product_type' => 'required|in:single,variant,combo', 'variation_ids' => 'nullable|array|exists:variations,id', 'stocks.*.warehouse_id' => 'nullable|exists:warehouses,id', 'stocks.*.productStock' => 'nullable|numeric|min:0|max:99999999.99', 'stocks.*.exclusive_price' => 'nullable|numeric|min:0|max:99999999.99', 'stocks.*.inclusive_price' => 'nullable|numeric|min:0|max:99999999.99', 'stocks.*.profit_percent' => 'nullable|numeric|max:99999999.99', 'stocks.*.productSalePrice' => 'nullable|numeric|min:0|max:99999999.99', 'stocks.*.productWholeSalePrice' => 'nullable|numeric|min:0|max:99999999.99', 'stocks.*.productDealerPrice' => 'nullable|numeric|min:0|max:99999999.99', 'stocks.*.mfg_date' => 'nullable|date', 'stocks.*.expire_date' => 'nullable|date|after_or_equal:stocks.*.mfg_date', 'stocks' => 'nullable|array', 'stocks.*.batch_no' => [ 'nullable', function ($attribute, $value, $fail) use ($request) { $batchNos = collect($request->stocks)->pluck('batch_no')->filter()->toArray(); if (count($batchNos) !== count(array_unique($batchNos))) { $fail('Duplicate batch number found in the request.'); } }, ], // Combo validation 'combo_products' => 'nullable|array', 'combo_products.*.stock_id' => [ 'required_if:product_type,combo', Rule::exists('stocks', 'id')->where('business_id', $business_id), ], 'combo_products.*.quantity' => 'required_if:product_type,combo|numeric|min:1', ]); DB::beginTransaction(); try { // VAT calculation $vat = Vat::find($request->vat_id); $vat_rate = $vat->rate ?? 0; // Update product $product->update($request->except(['productPicture', 'productPurchasePrice', 'productDealerPrice', 'productWholeSalePrice', 'alert_qty', 'stocks', 'vat_amount', 'profit_percent', 'is_displayed_in_pos']) + [ 'business_id' => $business_id, 'alert_qty' => $request->alert_qty ?? 0, 'is_displayed_in_pos' => $request->has('is_displayed_in_pos') ? 1 : 0, 'profit_percent' => $request->profit_percent ?? 0, 'productPicture' => $request->productPicture ? $this->upload($request, 'productPicture', $product->productPicture) : $product->productPicture, ]); // Delete previous stocks and combos if ($product->product_type === 'combo') { ComboProduct::where('product_id', $product->id)->delete(); } // Handle Single/Variant Product Stocks if (in_array($request->product_type, ['single', 'variant']) && !empty($request->stocks)) { $existingStockIds = $product->stocks()->pluck('id')->toArray(); $incomingStockIds = collect($request->stocks)->pluck('stock_id')->filter()->toArray(); // Delete removed stocks $stocksToDelete = array_diff($existingStockIds, $incomingStockIds); Stock::whereIn('id', $stocksToDelete)->delete(); // Insert or Update foreach ($request->stocks as $stock) { $stockId = $stock['stock_id'] ?? null; // Recalculate price $base_price = $stock['exclusive_price'] ?? 0; $purchasePrice = $request->vat_type === 'inclusive' ? $base_price + ($base_price * $vat_rate / 100) : $base_price; $payload = [ 'business_id' => $business_id, 'product_id' => $product->id, 'batch_no' => $stock['batch_no'] ?? null, 'warehouse_id' => $stock['warehouse_id'] ?? null, 'productStock' => $stock['productStock'] ?? 0, 'productPurchasePrice' => $purchasePrice, 'profit_percent' => $stock['profit_percent'] ?? 0, 'productSalePrice' => $stock['productSalePrice'] ?? 0, 'productWholeSalePrice' => $stock['productWholeSalePrice'] ?? 0, 'productDealerPrice' => $stock['productDealerPrice'] ?? 0, 'mfg_date' => $stock['mfg_date'] ?? null, 'expire_date' => $stock['expire_date'] ?? null, 'variation_data' => $stock['variation_data'] ?? null, 'variant_name' => $stock['variant_name'] ?? null, 'branch_id' => auth()->user()->branch_id ?? auth()->user()->active_branch_id, 'serial_numbers' => $request->has_serial ? $stock['serial_numbers'] : null, ]; if ($stockId) { Stock::where('id', $stockId)->update($payload); } else { Stock::create($payload); } } } // Handle Combo Product if ($request->product_type === 'combo' && !empty($request->combo_products)) { foreach ($request->combo_products as $item) { ComboProduct::create([ 'product_id' => $product->id, 'stock_id' => $item['stock_id'], 'quantity' => $item['quantity'], 'purchase_price' => $item['purchase_price'] ?? 0, ]); } } DB::commit(); return response()->json([ 'message' => __('Product updated successfully.'), 'redirect' => route('business.products.index') ]); } catch (\Exception $e) { DB::rollback(); return response()->json([ 'message' => __('Something went wrong.'), 'error' => $e->getMessage(), ], 406); } } public function destroy($id) { $product = Product::findOrFail($id); if (file_exists($product->productPicture)) { Storage::delete($product->productPicture); } $product->delete(); return response()->json([ 'message' => __('Product deleted successfully'), 'redirect' => route('business.products.index') ]); } public function deleteAll(Request $request) { $products = Product::whereIn('id', $request->ids)->get(); foreach ($products as $product) { if (file_exists($product->productPicture)) { Storage::delete($product->productPicture); } } Product::whereIn('id', $request->ids)->delete(); return response()->json([ 'message' => __('Selected product deleted successfully'), 'redirect' => route('business.products.index') ]); } public function getAllProduct() { $products = Product::with([ 'stocks' => function ($query) { $query->where('productStock', '>', 0); }, 'category:id,categoryName', 'unit:id,unitName', 'stocks.warehouse:id,name', 'vat:id,rate' ]) ->where('business_id', auth()->user()->business_id) ->withSum('stocks as total_stock', 'productStock') ->latest() ->get() ->where('total_stock', '>', 0) ->values(); return response()->json($products); } public function getByCategory($category_id) { $products = Product::where('business_id', auth()->user()->business_id)->where('category_id', $category_id)->get(); return response()->json($products); } public function exportExcel() { return Excel::download(new ExportProduct, 'product.xlsx'); } public function exportCsv() { return Excel::download(new ExportProduct, 'product.csv'); } public function exportPdf() { $products = Product::with('unit:id,unitName', 'brand:id,brandName', 'category:id,categoryName') ->where('business_id', auth()->user()->business_id) ->withSum('stocks as total_stock', 'productStock') ->latest() ->get(); return PdfService::render('business::products.pdf', compact('products'), 'product-list.pdf'); } public function expiredProduct(Request $request) { $expired_products = Product::with('unit:id,unitName', 'brand:id,brandName', 'category:id,categoryName', 'stocks') ->where('business_id', auth()->user()->business_id) ->withSum('stocks as total_stock', 'productStock') ->whereHas('stocks', function ($query) { $query->whereDate('expire_date', '<', today()) ->where('productStock', '>', 0); }) ->when($request->search, function ($q) use ($request) { $q->where(function ($q) use ($request) { $q->where('type', 'like', '%' . $request->search . '%') ->orWhere('productName', 'like', '%' . $request->search . '%') ->orWhere('productCode', 'like', '%' . $request->search . '%') ->orWhere('productSalePrice', 'like', '%' . $request->search . '%') ->orWhere('productPurchasePrice', 'like', '%' . $request->search . '%') ->orWhereHas('unit', function ($q) use ($request) { $q->where('unitName', 'like', '%' . $request->search . '%'); }) ->orWhereHas('brand', function ($q) use ($request) { $q->where('brandName', 'like', '%' . $request->search . '%'); }) ->orWhereHas('category', function ($q) use ($request) { $q->where('categoryName', 'like', '%' . $request->search . '%'); }); }); }) ->latest() ->paginate($request->per_page ?? 20)->appends($request->query()); if ($request->ajax()) { return response()->json([ 'data' => view('business::expired-products.datas', compact('expired_products'))->render() ]); } return view('business::expired-products.index', compact('expired_products')); } public function exportExpireProductExcel() { return Excel::download(new ExportExpiredProduct, 'expired-product.xlsx'); } public function exportExpireProductCsv() { return Excel::download(new ExportExpiredProduct, 'expired-product.csv'); } public function show($id) { $business_id = auth()->user()->business_id; $product = Product::with('stocks')->where('business_id', $business_id)->findOrFail($id); $categories = Category::where('business_id', $business_id)->whereStatus(1)->latest()->get(); $brands = Brand::where('business_id', $business_id)->whereStatus(1)->latest()->get(); $units = Unit::where('business_id', $business_id)->whereStatus(1)->latest()->get(); $vats = Vat::where('business_id', $business_id)->latest()->get(); $profit_option = Option::where('key', 'business-settings') ->where('value', 'LIKE', '%"business_id":%' . $business_id . '%') ->get() ->firstWhere('value.business_id', $business_id)['product_profit_option'] ?? ''; return view('business::products.create-stock', compact('categories', 'brands', 'units', 'product', 'vats', 'profit_option')); } public function CreateStock(Request $request, string $id) { $product = Product::findOrFail($id); $business_id = auth()->user()->business_id; $request->validate([ 'vat_id' => 'nullable|exists:vats,id', 'vat_type' => 'nullable|in:inclusive,exclusive', 'productDealerPrice' => 'nullable|numeric|min:0', 'exclusive_price' => 'required|numeric|min:0', 'inclusive_price' => 'required|numeric|min:0', 'profit_percent' => 'nullable|numeric', 'productSalePrice' => 'required|numeric|min:0', 'productWholeSalePrice' => 'nullable|numeric|min:0', 'productStock' => 'required|numeric|min:0', 'expire_date' => 'nullable|date', 'batch_no' => 'nullable|string', 'productCode' => [ 'nullable', 'unique:products,productCode,' . $product->id . ',id,business_id,' . $business_id, ], ]); DB::beginTransaction(); try { // Calculate purchase price including VAT if applicable $vat = Vat::find($request->vat_id); $exclusive_price = $request->exclusive_price ?? 0; $vat_amount = ($exclusive_price * ($vat->rate ?? 0)) / 100; // Determine final purchase price based on VAT type $purchase_price = $request->vat_type === 'exclusive' ? $exclusive_price : $exclusive_price + $vat_amount; $batchNo = $request->batch_no ?? null; $stock = Stock::where(['batch_no' => $batchNo, 'product_id' => $product->id])->first(); if ($stock) { $stock->update($request->except('productStock', 'productPurchasePrice', 'productSalePrice', 'productDealerPrice', 'productWholeSalePrice') + [ 'productStock' => $stock->productStock + $request->productStock, 'productPurchasePrice' => $purchase_price, 'productSalePrice' => $request->productSalePrice, 'productDealerPrice' => $request->productDealerPrice ?? 0, 'productWholeSalePrice' => $request->productWholeSalePrice ?? 0, ]); } else { Stock::create($request->except('productStock', 'productPurchasePrice', 'productSalePrice', 'productDealerPrice', 'productWholeSalePrice') + [ 'product_id' => $product->id, 'branch_id' => auth()->user()->branch_id ?? auth()->user()->active_branch_id, 'business_id' => $business_id, 'productStock' => $request->productStock ?? 0, 'productPurchasePrice' => $purchase_price, 'productSalePrice' => $request->productSalePrice, 'productDealerPrice' => $request->productDealerPrice ?? 0, 'productWholeSalePrice' => $request->productWholeSalePrice ?? 0, ]); } DB::commit(); return response()->json([ 'message' => __('Data saved successfully.'), 'redirect' => route('business.products.index'), ]); } catch (\Exception $e) { DB::rollBack(); return response()->json([ 'message' => __('Something went wrong.'), ], 406); } } public function getShelf(Request $request) { $rack = Rack::with('shelves')->find($request->rack_id); return response()->json($rack ? $rack->shelves : []); } public function getProductVariants($product_id) { $variant_stocks = Stock::select('id', 'variant_name', 'batch_no', 'expire_date') ->where('business_id', auth()->user()->business_id) ->where('product_id', $product_id) ->get(); return response()->json($variant_stocks); } }