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

@ -84,6 +84,37 @@
@enderror
</div>
<div class="form-row">
<div class="form-group col-sm-4">
<label class="form-label" for="storage_room_id">{{ __('Lagerort: Raum') }}</label>
<select name="storage_room_id" id="storage_room_id" class="form-control">
<option value="">{{ __('— keine Angabe —') }}</option>
@foreach($storageRooms as $room)
<option value="{{ $room->id }}" @selected((string) old('storage_room_id', $model->storage_room_id) === (string) $room->id)>{{ $room->label }}</option>
@endforeach
</select>
</div>
<div class="form-group col-sm-4">
<label class="form-label" for="storage_shelf_id">{{ __('Lagerort: Regal') }}</label>
<select name="storage_shelf_id" id="storage_shelf_id" class="form-control">
<option value="">{{ __('— keine Angabe —') }}</option>
@foreach($storageShelves as $shelf)
<option value="{{ $shelf->id }}" @selected((string) old('storage_shelf_id', $model->storage_shelf_id) === (string) $shelf->id)>{{ $shelf->label }}</option>
@endforeach
</select>
</div>
<div class="form-group col-sm-4">
<label class="form-label" for="storage_slot_id">{{ __('Lagerort: Fach / Buchstabe') }}</label>
<select name="storage_slot_id" id="storage_slot_id" class="form-control">
<option value="">{{ __('— keine Angabe —') }}</option>
@foreach($storageSlots as $slot)
<option value="{{ $slot->id }}" @selected((string) old('storage_slot_id', $model->storage_slot_id) === (string) $slot->id)>{{ $slot->label }}</option>
@endforeach
</select>
<small class="text-muted">{{ __('Bausteine zentral pflegbar unter Einstellungen → Allgemein.') }}</small>
</div>
</div>
<div class="form-group">
<label class="custom-control custom-checkbox">
<input type="checkbox" name="active" value="1" class="custom-control-input"

View file

@ -23,6 +23,7 @@
<th>{{ __('Material') }}</th>
<th>{{ __('Gewicht (g)') }}</th>
<th>{{ __('Lieferant') }}</th>
<th>{{ __('Lagerplatz') }}</th>
<th>{{ __('URL') }}</th>
<th>{{ __('Status') }}</th>
<th></th>
@ -40,6 +41,7 @@
<td>{{ $value->packagingMaterial?->name ?? '—' }}</td>
<td>{{ $value->weight_grams }}</td>
<td>{{ $value->supplier?->name ?? '—' }}</td>
<td>{{ $value->storageLabel() ?? '—' }}</td>
<td>
@if($value->url)
<a href="{{ $value->url }}" target="_blank" title="{{ $value->url }}"><i class="fa fa-external-link-alt"></i></a>

View file

@ -103,6 +103,9 @@
@if (!empty($product->number))
<div class="wawi-item-sub">{{ $product->number }}</div>
@endif
@if ($product->storageLabel())
<div class="wawi-item-sub text-muted"><span class="fas fa-map-marker-alt mr-1"></span>{{ $product->storageLabel() }}</div>
@endif
</td>
<td class="text-right {{ $stockClass }}">
{{ \App\Services\Util::formatNumber($row['stock'], 0) }} <span class="text-muted">{{ __('Stück') }}</span>

View file

@ -1140,6 +1140,38 @@
</div>
</div>
<hr>
<p class="text-muted small mb-2">{{ __('Lagerplatz (Raum / Regal / Fach). Bausteine zentral pflegbar unter Einstellungen → Allgemein.') }}</p>
<div class="form-row">
<div class="form-group col-sm-4">
<label class="form-label" for="storage_room_id">{{ __('Lagerort: Raum') }}</label>
<select name="storage_room_id" id="storage_room_id" class="form-control">
<option value="">{{ __('— keine Angabe —') }}</option>
@foreach ($storageRooms as $room)
<option value="{{ $room->id }}" @selected((string) old('storage_room_id', $product->storage_room_id) === (string) $room->id)>{{ $room->label }}</option>
@endforeach
</select>
</div>
<div class="form-group col-sm-4">
<label class="form-label" for="storage_shelf_id">{{ __('Lagerort: Regal') }}</label>
<select name="storage_shelf_id" id="storage_shelf_id" class="form-control">
<option value="">{{ __('— keine Angabe —') }}</option>
@foreach ($storageShelves as $shelf)
<option value="{{ $shelf->id }}" @selected((string) old('storage_shelf_id', $product->storage_shelf_id) === (string) $shelf->id)>{{ $shelf->label }}</option>
@endforeach
</select>
</div>
<div class="form-group col-sm-4">
<label class="form-label" for="storage_slot_id">{{ __('Lagerort: Fach / Buchstabe') }}</label>
<select name="storage_slot_id" id="storage_slot_id" class="form-control">
<option value="">{{ __('— keine Angabe —') }}</option>
@foreach ($storageSlots as $slot)
<option value="{{ $slot->id }}" @selected((string) old('storage_slot_id', $product->storage_slot_id) === (string) $slot->id)>{{ $slot->label }}</option>
@endforeach
</select>
</div>
</div>
<hr>
<p class="text-muted small mb-2">{{ __('Optional: dieses Einzelprodukt einem Hauptprodukt zuordnen (z. B. „50 × 15 ml"). Der Produktbestand führt nur Haupt-/Einzelprodukte.') }}</p>
<div class="form-row">