Anforderung 15.07.2026: Nahezu alle WaWi-Einträge sind jetzt löschbar (Soft-Delete, deleted_at), mit Abhängigkeitsprüfung und Bestätigungs- warnung vor jeder Löschung. - Migration: deleted_at auf 14 WaWi-Tabellen (Stammdaten, ingredients, stock_entries, stock_disposals, productions); SoftDeletes-Trait in den Models (suppliers/packaging_items hatten es bereits). - Neuer App\Services\DeletionGuard: zentrale Abhängigkeitsprüfung je Entität — referenzierte Einträge sind nicht löschbar, Fehlermeldung nennt die Verwendungen (behebt auch Audit M-4: kein 500er/stiller Cascade-Verlust mehr beim Löschen verwendeter Stammdaten). - SettingCrudController::destroy() prüft Blocker; Supplier-, PackagingItem-, Ingredient-destroy ebenso (Ingredient: vollständige Prüfung inkl. Produktion/Ausschuss, Route GET→DELETE, Audit M-6). - Wareneingänge: auch gebuchte Eingänge löschbar, sofern kein Produktions-Verbrauch/Ausschuss darauf gebucht ist. - Ausschuss: löschbar (Menge steht dem Bestand wieder zur Verfügung). - Produktionen: löschbar (admin >= 7); Produktbestand wird per Gegenbuchung zurückgeführt (reverseProductionStock, append-only, withTrashed), Rohstoffverbrauch zählt nicht mehr (Bestandsqueries schließen gelöschte Produktionen aus). - lastNetPricePerSupplier: DB::query statt Model-Query (Global Scope auf Sub-Select-Alias). - Debug-Log-Reste im PackagingItemController entfernt (Audit N-16). - Views: Lösch-Buttons mit deutschen Bestätigungswarnungen (Ausschuss, Produktionen, gebuchte Wareneingänge, INCI als DELETE-Formular). Tests: neuer InventoryDeletionTest (10 Tests), StorageSettingsTest an neue Löschregel angepasst. Volle Suite: 214 grün, 783 Assertions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
109 lines
3.5 KiB
PHP
Executable file
109 lines
3.5 KiB
PHP
Executable file
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Http\Requests\StoreIngredientRequest;
|
|
use App\Models\DeliveryTime;
|
|
use App\Models\Ingredient;
|
|
use App\Models\StorageRoom;
|
|
use App\Models\StorageShelf;
|
|
use App\Models\StorageSlot;
|
|
use App\Models\Supplier;
|
|
use App\Models\TaxRate;
|
|
use App\Services\DeletionGuard;
|
|
use Illuminate\Http\Request;
|
|
|
|
class IngredientController extends Controller
|
|
{
|
|
public function __construct()
|
|
{
|
|
$this->middleware('copyreader');
|
|
}
|
|
|
|
public function index(Request $request)
|
|
{
|
|
$onlyActive = $request->boolean('only_active');
|
|
|
|
$data = [
|
|
'values' => Ingredient::query()
|
|
->when($onlyActive, fn ($q) => $q->usedInActiveRecipes())
|
|
->get(),
|
|
'onlyActive' => $onlyActive,
|
|
];
|
|
|
|
return view('admin.ingredient.index', $data);
|
|
}
|
|
|
|
public function edit($id)
|
|
{
|
|
if ($id === 'new') {
|
|
$model = new Ingredient;
|
|
$model->active = true;
|
|
} else {
|
|
$model = Ingredient::with('suppliers')->findOrFail($id);
|
|
}
|
|
$data = [
|
|
'model' => $model,
|
|
'taxRates' => TaxRate::query()->active()->orderBy('pos')->orderBy('name')->get(),
|
|
'deliveryTimes' => DeliveryTime::query()->active()->orderBy('pos')->orderBy('label')->get(),
|
|
'suppliers' => Supplier::query()->where('active', true)->orderBy('name')->get(),
|
|
'storageRooms' => StorageRoom::query()->active()->orderBy('pos')->orderBy('label')->get(),
|
|
'storageShelves' => StorageShelf::query()->active()->orderBy('pos')->orderBy('label')->get(),
|
|
'storageSlots' => StorageSlot::query()->active()->orderBy('pos')->orderBy('label')->get(),
|
|
];
|
|
|
|
return view('admin.ingredient.edit', $data);
|
|
}
|
|
|
|
public function store(StoreIngredientRequest $request)
|
|
{
|
|
$data = $request->validated();
|
|
$data['default_factor'] = $data['default_factor'] ?? 1.10;
|
|
|
|
$supplierRows = $data['suppliers'] ?? [];
|
|
unset($data['suppliers']);
|
|
|
|
if ($request->input('id') === 'new') {
|
|
$model = Ingredient::create($data);
|
|
} else {
|
|
$model = Ingredient::findOrFail($request->input('id'));
|
|
$model->fill($data)->save();
|
|
}
|
|
|
|
// Repeater-Zeilen → Pivot-Sync; URL leer = Bestellung per Mail. Doppelte Lieferanten: letzte Zeile gewinnt.
|
|
$sync = [];
|
|
foreach ($supplierRows as $row) {
|
|
$supplierId = (int) ($row['id'] ?? 0);
|
|
if ($supplierId > 0) {
|
|
$url = trim((string) ($row['url'] ?? ''));
|
|
$sync[$supplierId] = ['url' => $url === '' ? null : $url];
|
|
}
|
|
}
|
|
$model->suppliers()->sync($sync);
|
|
|
|
\Session()->flash('alert-save', '1');
|
|
|
|
return redirect(route('admin_product_ingredients'));
|
|
|
|
}
|
|
|
|
public function delete($id)
|
|
{
|
|
$model = Ingredient::findOrFail($id);
|
|
|
|
// Anforderung 15.07.2026: Soft-Delete mit vollständiger Abhängigkeitsprüfung
|
|
// (Rezepturen, Wareneingänge, Ausschuss, Produktions-Verbräuche).
|
|
$guard = app(DeletionGuard::class);
|
|
$blockers = $guard->blockersFor($model);
|
|
if ($blockers !== []) {
|
|
\Session()->flash('alert-error', $guard->message($blockers));
|
|
|
|
return redirect(route('admin_product_ingredients'));
|
|
}
|
|
|
|
$model->delete();
|
|
\Session()->flash('alert-success', 'Eintrag gelöscht');
|
|
|
|
return redirect(route('admin_product_ingredients'));
|
|
}
|
|
}
|