where('active', true) ->with('materialQuality') ->orderBy('pos') ->orderBy('name') ->get(); $ids = $ingredients->pluck('id')->all(); $remaining = $this->inventoryService->remainingByIngredient($ids); $daily = $this->inventoryService->dailyConsumptionByIngredient($ids); $openOrders = $this->inventoryService->openOrderQuantityByIngredient($ids); $rows = $ingredients->map(function (Ingredient $ingredient) use ($remaining, $daily, $openOrders) { $rem = $remaining[$ingredient->id] ?? 0.0; $perDay = $daily[$ingredient->id] ?? null; $open = $openOrders[$ingredient->id] ?? 0.0; $minAlert = $ingredient->min_stock_alert !== null ? (float) $ingredient->min_stock_alert : null; $leadDays = $ingredient->delivery_time_days !== null ? (int) $ingredient->delivery_time_days : null; return [ 'ingredient' => $ingredient, 'remaining' => $rem, 'daily' => $perDay, 'open_order' => $open, 'days_until_empty' => $this->inventoryService->daysUntilEmpty($rem, $perDay), 'expected_empty' => $this->inventoryService->expectedEmptyDate($rem, $perDay), 'status' => $this->inventoryService->stockStatus($minAlert, $rem, $perDay, $leadDays, $open > 0), ]; }); return view('admin.inventory.raw-material-stock.index', [ 'rows' => $rows, 'criticalCount' => $rows->where('status', 'critical')->count(), 'horizonOptions' => $this->horizonOptions(), 'defaultHorizon' => 90, ]); } public function show(Ingredient $ingredient): View { $ingredient->load([ 'materialQuality', 'taxRate', 'suppliers' => fn ($q) => $q->orderByPivot('preferred', 'desc')->orderBy('name'), 'products' => fn ($q) => $q->where('active', true), ]); $entries = StockEntry::query() ->with(['supplier', 'location']) ->where('status', 'received') ->where('entry_type', 'ingredient') ->where('ingredient_id', $ingredient->id) ->orderByRaw('best_before is null, best_before asc') ->orderBy('id') ->get(); $consumed = $this->productionService->consumedByStockEntry($entries->pluck('id')->all()); $charges = $entries->map(function (StockEntry $entry) use ($consumed) { $received = $entry->received_quantity !== null ? (float) $entry->received_quantity : 0.0; $entry->setAttribute('remaining_quantity', round($received - ($consumed[(int) $entry->id] ?? 0.0), 2)); return $entry; })->filter(fn (StockEntry $entry) => (float) $entry->getAttribute('remaining_quantity') > 0.0) ->values(); $remaining = array_sum(array_map( fn (StockEntry $entry) => (float) $entry->getAttribute('remaining_quantity'), $charges->all() )); $remainingByLocation = $this->inventoryService->remainingByLocationForIngredient($ingredient->id); $openOrders = StockEntry::query() ->with(['supplier', 'location']) ->where('status', 'pending') ->where('entry_type', 'ingredient') ->where('ingredient_id', $ingredient->id) ->orderBy('ordered_at') ->orderBy('id') ->get(); $openTotal = round((float) $openOrders->sum('ordered_quantity'), 2); $daily = ($this->inventoryService->dailyConsumptionByIngredient([$ingredient->id]))[$ingredient->id] ?? null; $minAlert = $ingredient->min_stock_alert !== null ? (float) $ingredient->min_stock_alert : null; $leadDays = $ingredient->delivery_time_days !== null ? (int) $ingredient->delivery_time_days : null; $lastPriceBySupplier = $this->lastNetPricePerSupplier($ingredient->id); $productStock = $this->productStockService->currentStockByProduct($ingredient->products->pluck('id')->all()); return view('admin.inventory.raw-material-stock.show', [ 'ingredient' => $ingredient, 'productStock' => $productStock, 'charges' => $charges, 'remaining' => round($remaining, 2), 'remainingByLocation' => $remainingByLocation, 'locations' => Location::query()->where('active', true)->orderBy('name')->get(), 'openOrders' => $openOrders, 'openTotal' => $openTotal, 'daily' => $daily, 'daysUntilEmpty' => $this->inventoryService->daysUntilEmpty($remaining, $daily), 'expectedEmpty' => $this->inventoryService->expectedEmptyDate($remaining, $daily), 'status' => $this->inventoryService->stockStatus($minAlert, $remaining, $daily, $leadDays, $openTotal > 0), 'lastPriceBySupplier' => $lastPriceBySupplier, 'taxRates' => TaxRate::query()->active()->orderBy('pos')->orderBy('name')->get(), ]); } /** * Letzter Netto-kg-Preis je Lieferant für diesen Rohstoff (für die Bestell-/Lieferantenliste). * * @return array [supplier_id => price_per_kg_net] */ protected function lastNetPricePerSupplier(int $ingredientId): array { $entries = StockEntry::query() ->where('entry_type', 'ingredient') ->where('ingredient_id', $ingredientId) ->whereNotNull('supplier_id') ->whereNotNull('price_per_kg') ->orderBy('ordered_at', 'desc') ->orderBy('id', 'desc') ->get(['supplier_id', 'price_per_kg']); $result = []; foreach ($entries as $entry) { $supplierId = (int) $entry->supplier_id; if (! isset($result[$supplierId])) { $result[$supplierId] = (float) $entry->price_per_kg; } } return $result; } /** * @return array [days => label] */ protected function horizonOptions(): array { return [ 30 => __('Verbrauch (nächster Monat)'), 90 => __('Verbrauch (nächste 3 Monate)'), 180 => __('Verbrauch (nächste 6 Monate)'), 365 => __('Verbrauch (nächste 12 Monate)'), ]; } }