count(); $totalWarehouses = Warehouse::where('is_active', true)->count(); $recentTransactions = InventoryTransaction::with(['item', 'warehouse', 'createdBy']) ->orderBy('created_at', 'desc') ->limit(10) ->get(); // Low stock items (below min_stock or zero) $lowStockItems = Item::where('is_active', true) ->with(['inventoryTransactions']) ->get() ->filter(function ($item) { $stock = $this->getCurrentStockForItem($item->id); $minStock = $item->min_stock ?? 0; return $stock <= $minStock; }) ->take(10); return view('inventory.index', compact( 'totalItems', 'activeItems', 'totalWarehouses', 'recentTransactions', 'lowStockItems' )); } // ========================================== // ITEMS CRUD // ========================================== public function items(Request $request) { $query = Item::with(['inventoryTransactions.currency', 'inventoryTransactions.warehouse']); if ($request->filled('search')) { $search = $request->search; $query->where(function ($q) use ($search) { $q->where('name', 'like', "%{$search}%") ->orWhere('code', 'like', "%{$search}%") ->orWhere('category', 'like', "%{$search}%"); }); } if ($request->filled('category')) { $query->where('category', $request->category); } if ($request->filled('is_active')) { $query->where('is_active', $request->boolean('is_active')); } $items = $query->orderBy('name')->paginate(20)->appends($request->query()); $categories = Item::distinct()->pluck('category')->filter()->values(); // Calculate current stock for each item $items->through(function ($item) { $item->current_stock = $this->getCurrentStockForItem($item->id); return $item; }); return view('inventory.items', compact('items', 'categories')); } public function createItem() { $this->authorize('create', Item::class); $item = null; $categories = Item::distinct()->pluck('category')->filter()->values(); return view('inventory.item-form', compact('item', 'categories')); } public function storeItem(Request $request) { $this->authorize('create', Item::class); $validated = $request->validate([ 'name' => 'required|string|max:255', 'code' => 'nullable|string|max:50|unique:items,code', 'unit' => 'required|string|max:50', 'category' => 'nullable|string|max:100', 'description' => 'nullable|string', 'min_stock' => 'nullable|numeric|min:0', 'is_active' => 'boolean', ]); $validated['tenant_id'] = $this->getTenantId(); $validated['is_active'] = $request->boolean('is_active', true); $item = Item::create($validated); return redirect() ->route('inventory.items') ->with('success', __('inventory.item_created')); } public function editItem(Item $item) { $this->authorize('update', $item); $categories = Item::distinct()->pluck('category')->filter()->values(); return view('inventory.item-form', compact('item', 'categories')); } public function updateItem(Request $request, Item $item) { $this->authorize('update', $item); $validated = $request->validate([ 'name' => 'required|string|max:255', 'code' => 'nullable|string|max:50|unique:items,code,' . $item->id, 'unit' => 'required|string|max:50', 'category' => 'nullable|string|max:100', 'description' => 'nullable|string', 'min_stock' => 'nullable|numeric|min:0', 'is_active' => 'boolean', ]); $validated['is_active'] = $request->boolean('is_active'); $item->update($validated); return redirect() ->route('inventory.items') ->with('success', __('inventory.item_updated')); } public function destroyItem(Item $item) { $this->authorize('delete', $item); // Check if item has transactions if ($item->inventoryTransactions()->count() > 0) { return back()->withErrors(['item' => __('inventory.item_has_transactions')]); } $item->delete(); return redirect() ->route('inventory.items') ->with('success', __('inventory.item_deleted')); } // ========================================== // WAREHOUSES CRUD // ========================================== public function warehouses(Request $request) { $query = Warehouse::with(['project']); if ($request->filled('project_id')) { $query->where('project_id', $request->project_id); } if ($request->filled('is_active')) { $query->where('is_active', $request->boolean('is_active')); } $warehouses = $query->orderBy('name')->paginate(20)->appends($request->query()); $projects = Project::all(); return view('inventory.warehouses', compact('warehouses', 'projects')); } public function createWarehouse() { $this->authorize('create', Warehouse::class); $warehouse = null; $projects = Project::all(); return view('inventory.warehouse-form', compact('warehouse', 'projects')); } public function storeWarehouse(Request $request) { $this->authorize('create', Warehouse::class); $validated = $request->validate([ 'name' => 'required|string|max:255', 'location' => 'nullable|string|max:255', 'project_id' => 'nullable|exists:projects,id', 'is_active' => 'boolean', ]); $validated['tenant_id'] = $this->getTenantId(); $validated['is_active'] = $request->boolean('is_active', true); Warehouse::create($validated); return redirect() ->route('inventory.warehouses') ->with('success', __('inventory.warehouse_created')); } public function editWarehouse(Warehouse $warehouse) { $this->authorize('update', $warehouse); $projects = Project::all(); return view('inventory.warehouse-form', compact('warehouse', 'projects')); } public function updateWarehouse(Request $request, Warehouse $warehouse) { $this->authorize('update', $warehouse); $validated = $request->validate([ 'name' => 'required|string|max:255', 'location' => 'nullable|string|max:255', 'project_id' => 'nullable|exists:projects,id', 'is_active' => 'boolean', ]); $validated['is_active'] = $request->boolean('is_active'); $warehouse->update($validated); return redirect() ->route('inventory.warehouses') ->with('success', __('inventory.warehouse_updated')); } public function destroyWarehouse(Warehouse $warehouse) { $this->authorize('delete', $warehouse); if ($warehouse->inventoryTransactions()->count() > 0) { return back()->withErrors(['warehouse' => __('inventory.warehouse_has_transactions')]); } $warehouse->delete(); return redirect() ->route('inventory.warehouses') ->with('success', __('inventory.warehouse_deleted')); } // ========================================== // TRANSACTIONS // ========================================== public function transactions(Request $request) { $query = InventoryTransaction::with(['item', 'warehouse', 'project', 'currency', 'createdBy']); if ($request->filled('item_id')) { $query->where('item_id', $request->item_id); } if ($request->filled('warehouse_id')) { $query->where('warehouse_id', $request->warehouse_id); } if ($request->filled('type')) { $query->byType($request->type); } if ($request->filled('project_id')) { $query->where('project_id', $request->project_id); } if ($request->filled('from_date')) { $query->where('created_at', '>=', $request->from_date); } if ($request->filled('to_date')) { $query->where('created_at', '<=', $request->to_date); } $transactions = $query->orderBy('created_at', 'desc')->paginate(20)->appends($request->query()); $items = Item::where('is_active', true)->orderBy('name')->get(); $warehouses = Warehouse::where('is_active', true)->orderBy('name')->get(); $projects = Project::all(); $currencies = Currency::where('is_active', true)->get(); $types = ['in', 'out', 'transfer', 'return', 'adjustment']; return view('inventory.transactions', compact( 'transactions', 'items', 'warehouses', 'projects', 'currencies', 'types' )); } public function stockIn(Request $request) { $validated = $request->validate([ 'item_id' => 'required|exists:items,id', 'warehouse_id' => 'required|exists:warehouses,id', 'project_id' => 'nullable|exists:projects,id', 'quantity' => 'required|numeric|min:0.01', 'unit_price' => 'nullable|numeric|min:0', 'currency_id' => 'nullable|exists:currencies,id', 'reference_number' => 'nullable|string|max:100', 'notes' => 'nullable|string', ]); $validated['tenant_id'] = $this->getTenantId(); $validated['type'] = 'in'; $validated['created_by'] = Auth::id(); if (!empty($validated['unit_price']) && !empty($validated['quantity'])) { $validated['total_price'] = $validated['unit_price'] * $validated['quantity']; } InventoryTransaction::create($validated); return redirect() ->route('inventory.transactions') ->with('success', __('inventory.stock_in_success')); } public function stockOut(Request $request) { $validated = $request->validate([ 'item_id' => 'required|exists:items,id', 'warehouse_id' => 'required|exists:warehouses,id', 'project_id' => 'nullable|exists:projects,id', 'quantity' => 'required|numeric|min:0.01', 'unit_price' => 'nullable|numeric|min:0', 'currency_id' => 'nullable|exists:currencies,id', 'reference_number' => 'nullable|string|max:100', 'notes' => 'nullable|string', ]); $currentStock = $this->getCurrentStockForItem($validated['item_id'], $validated['warehouse_id']); if ($currentStock < $validated['quantity']) { return back() ->withErrors(['quantity' => __('inventory.insufficient_stock')]) ->withInput(); } $validated['tenant_id'] = $this->getTenantId(); $validated['type'] = 'out'; $validated['created_by'] = Auth::id(); if (!empty($validated['unit_price']) && !empty($validated['quantity'])) { $validated['total_price'] = $validated['unit_price'] * $validated['quantity']; } InventoryTransaction::create($validated); return redirect() ->route('inventory.transactions') ->with('success', __('inventory.stock_out_success')); } public function stockTransfer(Request $request) { $validated = $request->validate([ 'item_id' => 'required|exists:items,id', 'from_warehouse_id' => 'required|exists:warehouses,id', 'to_warehouse_id' => 'required|exists:warehouses,id|different:from_warehouse_id', 'project_id' => 'nullable|exists:projects,id', 'quantity' => 'required|numeric|min:0.01', 'notes' => 'nullable|string', ]); // Check source stock $currentStock = $this->getCurrentStockForItem($validated['item_id'], $validated['from_warehouse_id']); if ($currentStock < $validated['quantity']) { return back() ->withErrors(['quantity' => __('inventory.insufficient_stock_source')]) ->withInput(); } DB::transaction(function () use ($validated) { $tenantId = $this->getTenantId(); $userId = Auth::id(); // Out from source InventoryTransaction::create([ 'tenant_id' => $tenantId, 'item_id' => $validated['item_id'], 'warehouse_id' => $validated['from_warehouse_id'], 'project_id' => $validated['project_id'] ?? null, 'type' => 'transfer', 'quantity' => $validated['quantity'], 'reference_number' => 'TRF-' . time(), 'notes' => ($validated['notes'] ?? '') . ' — انتقال به انبار مقصد', 'created_by' => $userId, ]); // In to destination InventoryTransaction::create([ 'tenant_id' => $tenantId, 'item_id' => $validated['item_id'], 'warehouse_id' => $validated['to_warehouse_id'], 'project_id' => $validated['project_id'] ?? null, 'type' => 'transfer', 'quantity' => $validated['quantity'], 'reference_number' => 'TRF-' . time(), 'notes' => ($validated['notes'] ?? '') . ' — انتقال از انبار مبدأ', 'created_by' => $userId, ]); }); return redirect() ->route('inventory.transactions') ->with('success', __('inventory.transfer_success')); } public function currentStock(Request $request) { $request->validate([ 'item_id' => 'required|exists:items,id', 'warehouse_id' => 'nullable|exists:warehouses,id', ]); $stock = $this->getCurrentStockForItem($request->item_id, $request->warehouse_id); return response()->json([ 'item_id' => $request->item_id, 'warehouse_id' => $request->warehouse_id, 'current_stock' => $stock, ]); } // ========================================== // STOCK HELPERS // ========================================== private function getCurrentStockForItem(int $itemId, ?int $warehouseId = null): float { $query = InventoryTransaction::where('item_id', $itemId); if ($warehouseId) { $query->where('warehouse_id', $warehouseId); } $totalIn = (clone $query)->whereIn('type', ['in', 'return'])->sum('quantity'); $totalOut = (clone $query)->where('type', 'out')->sum('quantity'); $adjustments = (clone $query)->where('type', 'adjustment')->sum('quantity'); return (float) ($totalIn - $totalOut + $adjustments); } }