query('scope') === 'recipe' ? 'recipe' : 'all'; $ingredients = Ingredient::query() ->where('active', true) ->where('exclude_from_stock', false) ->when($scope === 'recipe', fn ($q) => $q->usedInActiveRecipes()) ->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, // Audit N-4: gleiche Zählweise wie die Kacheln (kritisch inkl. bereits bestellt). 'criticalCount' => $rows->whereIn('status', ['critical', 'critical_ordered'])->count(), 'horizonOptions' => $this->horizonOptions(), 'defaultHorizon' => 90, 'scope' => $scope, ]); } 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), ]); // AP-27: Chargen je batch_number zusammengeführt (jeder Einkauf bleibt eigener stock_entry). $chargeGroups = $this->inventoryService->chargeGroupsForIngredient($ingredient->id); // Audit H-1: Kopfwert aus der Gesamt-Aggregation (zieht auch Ausschuss // ohne Chargenbezug ab) — identisch zur Index-Seite, nicht Chargen-Summe. $remaining = $this->inventoryService->remainingByIngredient([$ingredient->id])[$ingredient->id] ?? 0.0; $remainingByLocation = $this->inventoryService->remainingByLocationForIngredient($ingredient->id); $openOrders = $this->inventoryService->openOrdersForIngredient($ingredient->id); $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->stockEntryRepository->lastNetPricePerSupplier($ingredient->id); $productStock = $this->productStockService->currentStockByProduct($ingredient->products->pluck('id')->all()); return view('admin.inventory.raw-material-stock.show', [ 'ingredient' => $ingredient, 'productStock' => $productStock, 'chargeGroups' => $chargeGroups, '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(), ]); } /** * @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)'), ]; } }