Warenwirtschaft: AP-51 — Lagerplatz-Felder für Produkte & Verpackung

Lagerplatz-Zuordnung (Raum/Regal/Fach) gab es bisher nur bei Rohstoffen. Jetzt
dieselbe Storage-Hierarchie (StorageRoom/Shelf/Slot) auch für Produkte und
Verpackungsartikel, inkl. Anzeige an den passenden Stellen.

- Migration: storage_room_id/shelf/slot an products + packaging_items
  (nullable, FK constrained, nullOnDelete).
- Models Product + PackagingItem: fillable + storageRoom/Shelf/Slot-Relationen +
  storageLabel() („Raum 1 | Regal 3 | A").
- Produktformular (Inventar-Abschnitt) + Verpackungsformular: Auswahl-Zeile
  Raum/Regal/Fach; Optionen kommen aktiv/sortiert aus den zentral pflegbaren
  Bausteinen (Einstellungen → Allgemein).
- Speichern: ProductRepository normalisiert leere Auswahl -> null (FK);
  PackagingItem-Requests + Repository um die drei Felder erweitert.
- Anzeige: Produktbestand (Sub-Zeile mit Marker), Produktverpackung-Übersicht
  (neue Spalte „Lagerplatz").
- DeletionGuard: Lagerbaustein ist nicht löschbar, solange Produkte/Verpackung
  darauf verweisen (analog Rohstoffe).

Tests: 4 neue (Produkt speichern + leer->null, Verpackung speichern + Anzeige,
Löschsperre, freier Baustein löschbar). Migration auf Dev-/Live-DB gelaufen.
Suite 331 grün.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-08-28 09:46:40 +00:00
parent eb47c8a82d
commit 30d4e28696
16 changed files with 363 additions and 5 deletions

View file

@ -7,10 +7,14 @@ use App\Http\Requests\Inventory\StorePackagingItemRequest;
use App\Http\Requests\Inventory\UpdatePackagingItemRequest;
use App\Models\PackagingItem;
use App\Models\PackagingMaterial;
use App\Models\StorageRoom;
use App\Models\StorageShelf;
use App\Models\StorageSlot;
use App\Models\Supplier;
use App\Repositories\PackagingItemRepository;
use App\Services\DeletionGuard;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
class PackagingItemController extends Controller
{
@ -24,7 +28,7 @@ class PackagingItemController extends Controller
$isShipping = $category === 'shipping';
$query = PackagingItem::query()
->with(['packagingMaterial', 'supplier'])
->with(['packagingMaterial', 'supplier', 'storageRoom', 'storageShelf', 'storageSlot'])
->orderBy('name');
if ($isShipping) {
@ -52,9 +56,24 @@ class PackagingItemController extends Controller
'packagingMaterials' => PackagingMaterial::query()->orderBy('pos')->orderBy('name')->get(),
'suppliers' => Supplier::query()->orderBy('name')->get(),
'category' => $category,
...$this->storageOptions(),
]);
}
/**
* AP-51: Lagerplatz-Bausteine (Raum/Regal/Fach) für das Formular.
*
* @return array<string, Collection<int, mixed>>
*/
private function storageOptions(): array
{
return [
'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(),
];
}
public function store(StorePackagingItemRequest $request)
{
$validated = $request->validated();
@ -77,6 +96,7 @@ class PackagingItemController extends Controller
'packagingMaterials' => PackagingMaterial::query()->orderBy('pos')->orderBy('name')->get(),
'suppliers' => Supplier::query()->orderBy('name')->get(),
'category' => $category,
...$this->storageOptions(),
]);
}

View file

@ -25,7 +25,7 @@ class ProductStockController extends Controller
->where('is_set', false)
->where('show_in_product_stock', true)
->whereNull('main_product_id')
->with('images')
->with(['images', 'storageRoom', 'storageShelf', 'storageSlot'])
->orderBy('pos')
->orderBy('name')
->get();

View file

@ -8,6 +8,9 @@ use App\Models\PackagingItem;
use App\Models\Product;
use App\Models\ProductImage;
use App\Models\ProductIngredient;
use App\Models\StorageRoom;
use App\Models\StorageShelf;
use App\Models\StorageSlot;
use App\Repositories\ProductRepository;
use Illuminate\Database\Eloquent\Collection;
use Request;
@ -58,6 +61,9 @@ class ProductController extends Controller
'ingredient_catalog' => Ingredient::query()->where('active', true)->with('materialQuality')->orderBy('name')->get(['id', 'name', 'inci', 'effect', 'default_factor', 'material_quality_id']),
'packaging_catalog' => PackagingItem::query()->where('active', true)->with('packagingMaterial')->orderBy('name')->get(),
'set_product_catalog' => $this->setProductCatalog($model),
'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.product.edit', $data);
@ -127,6 +133,9 @@ class ProductController extends Controller
'ingredient_catalog' => Ingredient::query()->where('active', true)->with('materialQuality')->orderBy('name')->get(['id', 'name', 'inci', 'effect', 'default_factor', 'material_quality_id']),
'packaging_catalog' => PackagingItem::query()->where('active', true)->with('packagingMaterial')->orderBy('name')->get(),
'set_product_catalog' => $this->setProductCatalog($model),
'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(),
];
if ($validator->fails()) {