43 KiB
Entwicklungsplan: Warenwirtschaft & Produktion
Version: 2.0 - Stand 10.04.2026 Referenz:
konzept-final.md(V1.1),feedback.md(Kundin, April 2026) Geschaetzte Gesamtdauer: 12-15 Wochen (1 Entwickler)
Umsetzungsprotokoll (Ist-Stand)
| Datum | Phase | Kurzbeschreibung |
|---|---|---|
| 27.03.2026 | 0 | Bugfixes Produkt/INCI, Sync-Logik, HTMLHelper-Sortierung, Kopie type_id; Pest-Tests tests/Feature/ProductPhase0Test.php; zwei Migrationskorrekturen (siehe Phase 0). |
| 27.03.2026 | 1 | Migrationen Rezeptur/Haltbarkeit/Stammdaten INCI; UI Produktformular SortableJS (CDN) + Select2; Pivot pos/gram/factor; Warenwirtschaft Haltbarkeit; product_inci_sync_sent; Ingredient-Formular default_factor/min_stock_alert; Tests ProductPhase1Test.php. |
| 27.03.2026 | 2 | Stammdaten-CRUD unter admin/inventory: Models/Relations, Repositories SupplierRepository/PackagingItemRepository, Resource-Controller + App\Http\Requests\Inventory\*, Views DataTables + Select2, Sidenav; Migrationen Pivot-Unique kurz (supplier_supplier_cat_unique), packaging_items.product_id als unsignedInteger (Kompatibilitaet products.id); Seeder InventoryStammdatenSeeder; Tests InventoryPhase2Test.php; layouts.layout-2 ergaenzt @yield('scripts'). |
| 27.03.2026 | 2 | Erweiterung InventoryPhase2Test.php: vollstaendiges CRUD je Stammdaten-Entitaet (inkl. Soft-Delete Lieferant/Verpackungsartikel, n:m Lieferant-Kategorien). |
| 27.03.2026 | 3 | Wareneingang: stock_entries, StockEntry, StockEntryRepository, StockEntryController, FormRequests, Routen + API-Suche (Select2), Views index/create/edit/show + _scripts (Select2/toggle), Sidenav-Link fuer CopyReader; Tests InventoryPhase3Test.php. |
| 27.03.2026 | 4 | Pivot product_packagings (quantity, pos), Product::packagings() / PackagingItem::products(), ProductRepository::updatePackagings + Kopie; Produktformular Abschnitt „Verpackung & Material“ mit Modal, SortableJS; Tests ProductPhase4Test.php. |
| 27.03.2026 | 5 | Produktion: Tabellen productions, production_ingredients, production_packagings; ProductionService (Transaktion, Soll/Ist Gramm, MHD-Warnung, Packaging-Snapshot); ProductionController + Rezept-API; Views index/create/show; Sidenav; Tests ProductionPhase5Test.php. |
| 6–9 | noch offen |
| 10.04.2026 | 5.1a | Feedback Menue & Stammdaten: Sidenav-Umbenennungen (Rohstoffqualitaet, Verpackungsmaterial, Produktverpackung, Versandverpackung, Stammdaten→Lieferanten/Kategorien); View-Titel; Lieferant Default-Land DE; PackagingItem URL-Feld + Kategorie-Split. |
| 10.04.2026 | 5.1b | Feedback Rezeptur & INCI: Ingredient material_quality_id + Anzeige im Produktformular; Rezeptur Gramm→Prozent (3 Nachkomma) mit 100%-Summe + Warnung; Hersteller-Rezeptur Kasten. |
| 10.04.2026 | 5.1c | Feedback Produktion: edit/update + copy-Funktion; nur aktive Produkte; Default-Lagerort Koeln. |
|| | 6–9 | noch offen |
Hinweis: Diese Tabelle bei jeder abgeschlossenen Teil-Lieferung um eine Zeile ergaenzen.
Uebersicht der Phasen
| Phase | Titel | Status | Geschaetzte Dauer | Abhaengigkeit |
|---|---|---|---|---|
| 0 | Bugfixes & Vorbereitung | erledigt (27.03.2026) | 2-3 Tage | - |
| 1 | INCI-Management Erweiterung | erledigt (27.03.2026) | 4-5 Tage | Phase 0 |
| 2 | Stammdaten-Module | umgesetzt (CRUD/Stammdaten, 27.03.2026) | 5-6 Tage | - |
| 3 | Wareneingang | umgesetzt (27.03.2026) | 5-7 Tage | Phase 2 |
| 4 | Rezeptur, BOM & Produkterweiterung | umgesetzt (27.03.2026) | 3-4 Tage | Phase 1 + 2 |
| 5 | Produktion & Bestandsabzug | umgesetzt (MVP 27.03.2026) | 6-8 Tage | Phase 3 + 4 |
| 5.1 | Feedback-Korrekturen (Menue, Rezeptur, Produktion) | umgesetzt (10.04.2026) | 5-7 Tage | Phase 1-5 |
| 6 | Bestandsuebersicht & Alarme | offen | 4-5 Tage | Phase 3 + 5 |
| 7 | Ausgang / Ausschuss | offen | 2-3 Tage | Phase 2 + 6 |
| 8 | Rollen, Audit-Trail & Feinschliff | offen | 3-4 Tage | Alle Phasen |
| 9 | Einstellungen & Konfiguration | offen | 1-2 Tage | Phase 6 |
Phase 0 --> Phase 1 --> Phase 4 --> Phase 5 --> Phase 6 --> Phase 8
/ / /
Phase 2 --> Phase 3 ------------- Phase 7 + 9 -------
Phase 0: Bugfixes & Vorbereitung (2-3 Tage)
Ziel: Solide Grundlage schaffen, bestehende Fehler beseitigen.
Status: Abgeschlossen (Umsetzung 27.03.2026)
Aufgaben
0.1 - Bugfix: Product/Ingredient hasMany-Relationen — [x] erledigt
- Dateien:
app/Models/Product.php,app/Models/Ingredient.php - Problem:
product_ingredients()hasMany hatte einen falschen Foreign-Key-Namen ('product_ingredients'/'id'statt echter FK-Spalte). - Ist-Umsetzung:
hasMany(ProductIngredient::class, 'product_id')bzw.hasMany(..., 'ingredient_id').
0.2 - Bugfix: updateIngredients() Sync-Logik — [x] erledigt
- Datei:
app/Repositories/ProductRepository.php - Problem: Nur Hinzufuegen, kein Entfernen von INCIs.
- Ist-Umsetzung:
$this->model->p_ingredients()->sync($ids)mit gefilterten Integer-IDs (nur positive IDs).
0.3 - Bugfix: Alphabetische Sortierung Dropdown — [x] erledigt
- Datei:
app/Services/HTMLHelper.php(KlasseHTMLHelperliegt im Projekt unter Services, nicht unterLibraries) - Ist-Umsetzung:
Ingredient::where('active', 1)->orderBy('name')->get()ingetProductIngredientsOptions().
0.4 - Bugfix: Attribute-Kopierung — [x] erledigt
- Datei:
app/Repositories/ProductRepository.php(Methodecopy()) - Ist-Umsetzung:
type_idaus der Quell-ZeileProductAttribute:$attribute->type_id(statt nicht existierendem Feld amProduct).
0.5 - Migrationskorrekturen (Tests / Schema-Sauberkeit) — [x] erledigt
Nicht im urspruenglichen Plan; noetig damit
php artisan testmit SQLite (In-Memory) und frische Installationen konsistent laufen.
| Migration | Problem | Ist-Umsetzung |
|---|---|---|
2018_09_29_145909_create_countries_table.php |
Spalte active war doppelt definiert |
Duplikat entfernt |
2021_06_15_112357_create_product_buys_table.php |
FK auf user_id, Spalte heisst auth_user_id |
FK auf auth_user_id korrigiert |
Hinweis fuer bestehende Produktions-DBs: Diese Dateien wurden bei laufenden Systemen typischerweise schon einmal migriert; die Korrekturen gelten vor allem fuer neue Deployments und die Test-Pipeline.
Tests — [x] umgesetzt
Datei: tests/Feature/ProductPhase0Test.php (Pest)
| Geplanter Aspekt | Ist-Abdeckung |
|---|---|
Relation Product → product_ingredients |
Test: korrekte FK-Verknuepfung |
Relation Ingredient → product_ingredients |
Test: korrekte FK-Verknuepfung |
| INCIs entfernen (Sync) | Test: updateIngredients mit zwei, dann einer ID |
| Dropdown alphabetisch | Test: Reihenfolge Alpha vor Zebra im HTML-Output |
| Produktkopie Attribute | Test: type_id der ProductAttribute-Zeile bleibt erhalten |
Nicht als separater HTTP-Feature-Test umgesetzt (optional spaeter): Formular-POST admin/product/store mit INCI-Auswahl — funktional durch Repository-Sync abgedeckt.
Deliverable
Bereinigte Codebasis ohne die in Phase 0 adressierten Bugs im Produkt/INCI-Bereich; automatisierte Regressionstests fuer die genannten Punkte.
Phase 1: INCI-Management Erweiterung (4-5 Tage)
Ziel: INCI-Verwaltung laut Briefing: Sortierung, Gramm, Faktor, Haltbarkeit, Kopierfunktion.
Status: Abgeschlossen (Umsetzung 27.03.2026)
Vorgaben Umsetzung (abgestimmt): SortableJS per CDN in
resources/views/admin/product/edit.blade.php(@section('scripts')); Select2 fuer Einzel-Auswahl „Inhaltsstoff hinzufuegen“ (Zeilen werden per JS angehaengt; Reihenfolge per Drag & Drop).
Aufgaben
1.1 - Migration: product_ingredients erweitern — [x] erledigt
- Datei:
database/migrations/2026_03_27_111352_add_recipe_fields_to_product_ingredients_table.php - Spalten:
pos,gram(nullable),factor(Default 1.10)
1.2 - Migration: ingredients erweitern — [x] erledigt
- Datei:
database/migrations/2026_03_27_111354_add_inventory_fields_to_ingredients_table.php - Spalten:
default_factor,min_stock_alert
1.3 - Migration: products erweitern (Haltbarkeit) — [x] erledigt
- Datei:
database/migrations/2026_03_27_111353_add_shelf_life_to_products_table.php - Spalten:
shelf_life_type(ENUMpao/fixed, nullable),shelf_life_months(nullable)
1.4 - Model: ProductIngredient aktualisieren — [x] erledigt
fillable+castsfuerpos,gram,factorProduct::p_ingredients()/Ingredient::products():withPivot('id','pos','gram','factor'),orderByPivot('pos')
1.5 - Model: Ingredient aktualisieren — [x] erledigt
fillable+casts; UI:resources/views/admin/ingredient/form.blade.php+ Speicherlogik inIngredientController::store()(Zahlen mitreFormatNumber)
1.6 - Frontend: SortableJS Drag & Drop — [x] erledigt
- Datei:
resources/views/admin/product/edit.blade.php(CDN SortableJS 1.15.2), Ziel#ingredient-sortable-rows
1.7 - Frontend: Select2 — [x] erledigt
- Einzel-Select
#ingredient-add-select; Katalog kommt ausProductController::edit()alsingredient_catalog(alphabetisch)
1.8 - Frontend: Gramm & Faktor Felder — [x] erledigt
- Pro Zeile
pi_gram[],pi_factor[]; Effektiv-Spalte per jQuery; Default-Faktor beim Hinzufuegen aus JSON-Katalog (default_factor), ohne zusaetzlichen AJAX-Endpoint
1.9 - Frontend: Haltbarkeits-Auswahl — [x] erledigt
- Karte Warenwirtschaft in
admin/product/form.blade.php: Radios keine Angabe / PAO / festes MHD; Monats-Dropdown nur bei „festes MHD“ (JS-Toggle)
1.10 - Backend: ProductRepository aktualisieren — [x] erledigt
updateIngredients(): parallele Arrayspi_ingredient_id,pi_gram,pi_factor;sync()mit Pivot; Hiddenproduct_inci_sync_sent: verhindert, dass Speichern ohne INCI-Bereich alle INCIs loescht; Legacy-Pfadproduct_ingredients[](ohne Hidden) fuer Tests/Altkundenupdate(): Normalisierungshelf_life_type/shelf_life_monthscopy(): kopiertpos,gram,factorsowie Haltbarkeit (viareplicate())
Tests — [x] umgesetzt
Dateien: tests/Feature/ProductPhase1Test.php, ergaenzend ProductPhase0Test.php
| Geplanter Aspekt | Ist-Abdeckung |
|---|---|
| Gramm/Faktor/Haltbarkeit speichern | Test mit product_inci_sync_sent + reFormatNumber-Eingaben |
| Produkt-Kopie pos/gram/factor (+ PAO) | Test ProductPhase1Test zweiter Fall |
Nicht separat als E2E-Test: reine Reihenfolge nur Drag & Drop im Browser (Speicherung erfolgt ueber Reihenfolge der pi_*-Arrays).
Deliverable
Erweitertes INCI-Management mit Rezeptur-Spalten, sortierbarer Tabelle, Haltbarkeit am Produkt und angepasstem Inhaltsstoff-Stammdaten-Formular.
Phase 2: Stammdaten-Module (5-6 Tage)
Ziel: Alle Stammdaten-Tabellen und CRUD-Oberflaechen fuer die Warenwirtschaft.
Status: Stammdaten-CRUD & Rechte umgesetzt (27.03.2026). Routen liegen in
routes/web.phpinnerhalb der bestehendenauth:user-Gruppe, verschachtelt mitsuperadminbzw.admin(analog zum Plan, nur ohne separaten aeusserenauth:user-Wrapper).
Aufgaben
2.1 - Modelle & Migrations erstellen
php artisan make:model Location -mf --no-interaction
php artisan make:model SupplierCategory -mf --no-interaction
php artisan make:model Supplier -mf --no-interaction
php artisan make:model MaterialQuality -mf --no-interaction
php artisan make:model PackagingMaterial -mf --no-interaction
php artisan make:model PackagingItem -mf --no-interaction
- Tabellenstrukturen laut
konzept-final.mdAbschnitt 3 - Factories mit sinnvollen Faker-Daten
- Pivot-Tabelle
supplier_supplier_categoryfuer n:m Lieferant-Kategorie-Beziehung - Supplier:
belongsToMany(SupplierCategory::class)stattbelongsTo
2.2 - Seeder: Vorbelegte Stammdaten
php artisan make:seeder InventoryStammdatenSeeder --no-interaction
- Lagerorte: Koeln, Waldboel
- Qualitaeten: konventionell, bio kaltgepresst, bio raffiniert, konventionell kaltgepresst, konventionell raffiniert
- Verpackungs-Materialien: Glas, Holz/Bambus, Pappe/Papier, Kunststoff
2.3 - Repositories erstellen
SupplierRepository- CRUD fuer Lieferanten inkl. Kategorie-Sync (n:m)PackagingItemRepository- CRUD fuer Verpackungs-Artikel- Einfache Stammdaten (Locations, Qualitaeten etc.) koennen direkt im Controller verarbeitet werden (kein eigenes Repository noetig bei reinem CRUD)
2.4 - Controller & FormRequests
php artisan make:controller Admin/Inventory/LocationController --resource --no-interaction
php artisan make:controller Admin/Inventory/SupplierCategoryController --resource --no-interaction
php artisan make:controller Admin/Inventory/SupplierController --resource --no-interaction
php artisan make:controller Admin/Inventory/MaterialQualityController --resource --no-interaction
php artisan make:controller Admin/Inventory/PackagingMaterialController --resource --no-interaction
php artisan make:controller Admin/Inventory/PackagingItemController --resource --no-interaction
- Pro Controller eine
StoreRequestundUpdateRequestFormRequest-Klasse - SupplierController nutzt
SupplierRepository
2.5 - Routes
- Neue Route-Gruppe in
routes/web.php:
// Warenwirtschaft - Stammdaten (SuperAdmin)
Route::middleware(['auth:user', 'superadmin'])->prefix('admin/inventory')->group(function () {
Route::resource('locations', LocationController::class);
Route::resource('material-qualities', MaterialQualityController::class);
Route::resource('packaging-materials', PackagingMaterialController::class);
});
// Warenwirtschaft - Stammdaten (Admin)
Route::middleware(['auth:user', 'admin'])->prefix('admin/inventory')->group(function () {
Route::resource('supplier-categories', SupplierCategoryController::class);
Route::resource('suppliers', SupplierController::class);
Route::resource('packaging-items', PackagingItemController::class);
});
2.6 - Blade Views
- Fuer jede Stammdaten-Entitaet:
index.blade.php(DataTable) +form.blade.php(Create/Edit) - Pfad:
resources/views/admin/inventory/ - Layout:
@extends('layouts.layout-2')(konsistent mit bestehendem Admin) - Styling: Bootstrap 4 (konsistent)
- DataTables: jQuery DataTables (konsistent)
- Supplier-Formular: Select2 Mehrfachauswahl fuer Kategorien
2.7 - Menue-Integration (Sidenav)
- Datei:
resources/views/layouts/includes/layout-sidenav.blade.php - Neuer Menue-Block "Warenwirtschaft" nach "Produkte"
- Bedingte Anzeige je nach Rolle
- Zunaechst nur Stammdaten-Links (weitere in spaeteren Phasen)
Tests
tests/Feature/InventoryPhase2Test.php: je Stammdaten-Entitaet CRUD (Index/Create/Store/Edit/Update/Destroy) mit passender Rolle (SuperAdmin bzw. Admin); Lieferant inkl. n:m-Kategorien und Soft-Delete; Verpackungsartikel mit Produkt-/Lieferanten-Zuordnung und Leeren optionaler FKs beim Update; zusaetzlich Rechte (Admin kein SuperAdmin-Inventory, CopyReader kein Lieferanten-Index, Admin keinlocations.store).
Deliverable
Alle Stammdaten-Module mit vollstaendiger CRUD-Funktionalitaet und korrekter Rechtevergabe.
Phase 3: Wareneingang (5-7 Tage)
Ziel: Zweistufiges Eingangs-System (Pending -> Received).
Aufgaben
3.1 - Model & Migration
php artisan make:model StockEntry -mf --no-interaction
- Tabellenstruktur laut
konzept-final.md - Inkl.
unitENUM('gram','piece') Spalte
3.2 - Repository
StockEntryRepository- CRUD + Stufe-2-Buchung (receive())- Methoden:
create(),update(),receive(),getByStatus(),getForIngredient()
3.3 - Controller & FormRequests
php artisan make:controller Admin/Inventory/StockEntryController --resource --no-interaction
StoreStockEntryRequest- Validierung Stufe 1 (Einkauf)ReceiveStockEntryRequest- Validierung Stufe 2 (Eingang)- Separate Route/Methode
receive()fuer die Stufe-2-Buchung
3.4 - Routes
Route::middleware(['auth:user', 'copyreader'])->prefix('admin/inventory')->group(function () {
Route::resource('stock-entries', StockEntryController::class);
Route::put('stock-entries/{stockEntry}/receive', [StockEntryController::class, 'receive'])
->name('stock-entries.receive');
});
Hinweis: Einkauf anlegen (
store) wird ueber zusaetzliche Autorisierung aufadminbeschraenkt, da CopyReader (Ivonne) nur Stufe 2 (buchen) darf. Umsetzung im Controller via$this->authorize()oder Gate.
3.5 - Views
index.blade.php- Liste aller Eingaenge mit Status-Badges (orange/gruen)create.blade.php- Stufe-1-Formular (dynamische Felder je nach Kategorie via JS)show.blade.php- Detail-Ansicht + Stufe-2-Formular (Eingangsbuchung)- Inhaltsstoff-Feld: Select2 Autosuggest (AJAX-Suche)
3.6 - API-Endpoint: Ingredienten-Suche
- Route:
GET /admin/inventory/api/ingredients/search?q=shea - Gibt JSON mit matching Ingredients zurueck
- Fuer Select2 AJAX-Anbindung
Tests
- Feature-Test: Stufe 1 - Einkauf erstellen (Status pending) —
InventoryPhase3Test.php - Feature-Test: Stufe 2 - Wareneingang buchen (Status received)
- Feature-Test: unit wird korrekt gesetzt (gram/piece)
- Feature-Test: CopyReader kann Stufe 2, aber nicht Stufe 1 (Create-Form Redirect)
- Feature-Test: Admin kann Stufe 1 (store)
- Feature-Test: Sortierung: Pending oben, dann Received (
listForIndex)
Deliverable
Funktionierender Wareneingang mit Zwei-Stufen-Workflow und rollenbasiertem Zugriff.
Phase 4: Rezeptur, BOM & Produkterweiterung (3-4 Tage)
Ziel: Verpackung am Produkt zuweisen (Bill of Materials).
Status: Umgesetzt (27.03.2026) – Pivot
product_packagingsmitpos; UI wie INCI (Modal + SortableJS, kein separates Select2-Feld).
Aufgaben
4.1 - Migration: product_packagings
php artisan make:model ProductPackaging -m --no-interaction
- Spalten:
product_id,packaging_item_id,quantity(Default 1)
4.2 - Model-Relationen
Product::packagings()->belongsToMany(PackagingItem::class, 'product_packagings')->withPivot('quantity')PackagingItem::products()-> inverse Relation
4.3 - Frontend: Produkt-Formular erweitern
- Datei:
resources/views/admin/product/form.blade.php - Neuer Abschnitt "Verpackung & Material" nach den INCIs
- Select2-Dropdown: Packaging-Artikel auswaehlen
- Pro Zeile: Artikelname, Material (readonly), Gewicht (readonly), Menge (editierbar)
- Sortierbar per SortableJS
4.4 - Backend: ProductRepository erweitern
- Neue Methode
updatePackagings($data)- Sync-Logik fuerproduct_packagings copy()erweitern: Packaging-Zuweisungen mitkopieren
Tests
- Feature-Test: Packaging am Produkt zuweisen und speichern —
ProductPhase4Test.php - Feature-Test: Produkt-Kopie uebernimmt Packaging
- Feature-Test: Menge pro Packaging-Artikel wird gespeichert
Deliverable
Produkte haben eine vollstaendige Stueckliste (Rezeptur + Verpackung).
Phase 5: Produktion & Bestandsabzug (6-8 Tage)
Ziel: Produktionslauf erfassen, Chargen zuordnen, Bestaende abziehen.
Status: MVP umgesetzt (27.03.2026). Bestandsabzug ist rechnerisch ueber gespeicherte Verbrauchswerte vorbereitet; vollstaendige
InventoryService-Bilanz folgt Phase 6.
Aufgaben
5.1 - Modelle & Migrations
php artisan make:model Production -mf --no-interaction
php artisan make:model ProductionIngredient -m --no-interaction
php artisan make:model ProductionPackaging -m --no-interaction
production_packagings: Snapshot-Tabelle fuer Packaging-Verbrauch pro Produktion (Spalten:production_id,packaging_item_id,quantity_used)
5.2 - Repository & Service
ProductionRepository- CRUD fuer Produktions-EintraegeProductionService- Orchestriert den Produktions-Workflow:production_ingredients-Eintraege erstellenproduction_packagings-Snapshot erstellen- MHD-Check: Warnung wenn Rohstoff-MHD < Produkt-MHD
- Transaktionssicher (
DB::transaction())
5.3 - Controller & FormRequests
php artisan make:controller Admin/Inventory/ProductionController --resource --no-interaction
StoreProductionRequest- Validierung aller Felder inkl. Chargen-Zuordnung
5.4 - Routes
Route::middleware(['auth:user', 'copyreader'])->prefix('admin/inventory')->group(function () {
Route::resource('productions', ProductionController::class);
});
5.5 - AJAX-API: Produkt-Daten laden
- Route:
GET /admin/inventory/api/products/{id}/recipe - Gibt JSON zurueck: INCIs (mit Gramm, Faktor), Packagings, letzte 3 Chargen pro INCI am Standort
5.6 - Views
index.blade.php- Liste vergangener Produktionen (DataTable)create.blade.php- Produktions-Formular:- Produkt-Dropdown
- Datum (Default heute)
- Stueckzahl
- Standort-Dropdown
- INCI-Liste (automatisch geladen): Pro INCI Chargen-Dropdown (letzte 3, Mehrfachauswahl)
- Packaging-Liste (automatisch geladen, read-only)
show.blade.php- Detail-Ansicht einer Produktion
5.7 - Chargen-Splitting UI
- Wenn ein User zwei Chargen fuer denselben INCI waehlt, erscheinen zwei Mengen-Felder
- Validierung: Summe der Teilmengen = Gesamtverbrauch (gram x factor x stueckzahl)
Tests
- Feature-Test: Produktion erstellen mit Chargen-Zuordnung —
ProductionPhase5Test.php - Feature-Test: Bestandsabzug nach Produktion korrekt berechnet (Rohstoffe) — Phase 6 /
InventoryService - Feature-Test: Packaging-Snapshot wird korrekt erstellt
- Feature-Test: Chargen-Splitting (2 Chargen fuer 1 INCI) — UI unterstuetzt „Weitere Charge“; separater Assertions-Test optional
- Feature-Test: MHD-Warnung wird ausgeloest
- Feature-Test: Packaging wird korrekt abgezogen — Bestandslogik Phase 6
- Feature-Test: BOM-Aenderung nach Produktion aendert historische Bestaende nicht — Snapshot-Tabelle abgedeckt; Integrations-Test optional
- Unit-/Service-Test: ProductionService (Soll/Ist, API-Rezept)
Deliverable
Vollstaendiger Produktions-Workflow mit Chargen-Tracking, Packaging-Snapshots und automatischem Bestandsabzug.
Phase 5.1: Feedback-Korrekturen (5-7 Tage)
Ziel: Alle Kundenfeedback-Punkte aus
feedback.mdumsetzen, bevor Phase 6 beginnt.Status: Umgesetzt (10.04.2026)
Quelle: Feedback der Kundin (April 2026) zu den bisherigen Umsetzungen Phase 0-5.
5.1a — Menue & Stammdaten-Korrekturen
5.1a.1 — Menustruktur Sidenav umbenennen — [x] erledigt
- Datei:
resources/views/layouts/includes/layout-sidenav.blade.php - Aenderungen:
- „Materialqualitaeten" → „Rohstoffqualitaet"
- „Verpackungsmaterialien" → „Verpackungsmaterial" (Singular)
- Admin-Untermenue „Stammdaten" → „Lieferanten"; erster Punkt „Lieferanten-Kategorien" → „Kategorien"
- „Verpackungsartikel" → „Produktverpackung"
- Neuer Menuepunkt „Versandverpackung" (filtert auf Kategorie
shipping)
5.1a.2 — View-Titel anpassen — [x] erledigt
- Dateien: Views unter
resources/views/admin/inventory/material-qualities/index.blade.php+form.blade.php: „Materialqualitaet" → „Rohstoffqualitaet"packaging-materials/index.blade.php+form.blade.php: „Verpackungsmaterialien" → „Verpackungsmaterial"packaging-items/index.blade.php+form.blade.php: „Verpackungsartikel" → „Produktverpackung" / „Versandverpackung"
5.1a.3 — PackagingItem Kategorien bereinigen — [x] erledigt
- Problem: Aktuell ENUM
packaging|label|shipping_office. Feedback: „Etikett" und „Versand/Buero" gehoeren nicht unter Produktverpackung. - Loesung: Kategorien aufteilen in zwei Gruppen:
- Produktverpackung:
packaging(nur Verpackungsartikel fuer das Produkt) - Versandverpackung:
shipping(Versand- & Bueromaterial, Etiketten)
- Produktverpackung:
- Migration: ENUM aendern auf
packaging|shipping; bestehendelabelundshipping_officeaufshippingmigrieren - Routing: Separate Index-Routen fuer Produktverpackung (
?category=packaging) und Versandverpackung (?category=shipping)
5.1a.4 — PackagingItem URL-Feld — [x] erledigt
- Migration: Spalte
url(nullable string) anpackaging_itemsanfuegen - Model:
urlin$fillable - Formular: Neues Textfeld „URL (Onlineshop)" in
packaging-items/form.blade.php - Validierung:
nullable|url|max:500
5.1a.5 — Lieferant: Default Land Deutschland — [x] erledigt
- Datei:
resources/views/admin/inventory/suppliers/form.blade.php - Default-Wert fuer
country_idbeim Erstellen: ID von Deutschland (Country::where('code', 'DE')->first()->id) - Controller
create():$defaultCountryIdmitgeben
5.1a.6 — Verpackungsartikel Produkt-Zuordnung — [x] erledigt
- Feedback: Dropdown unten (Produkt) ist optional; wenn behalten → Mehrfachauswahl
- Loesung:
product_id(FK Singular) durch n:m-Relation ersetzen ODER komplett entfernen - Empfehlung: Entfernen, da Produkt-Verpackungs-Zuordnung bereits ueber
product_packagings(Phase 4) abgebildet ist;product_idam PackagingItem ist redundant
5.1b — Rezeptur & INCI-Korrekturen
5.1b.1 — INCI: Qualitaet (material_quality_id) — [x] erledigt
- Migration: Spalte
material_quality_id(nullable FK) aningredients-Tabelle - Model:
IngredienterhaeltbelongsTo(MaterialQuality::class)Relation;material_quality_idin$fillable - Formular:
resources/views/admin/ingredient/form.blade.php— neues Select2-Dropdown „Rohstoffqualitaet" (Daten ausmaterial_qualities) - Produktformular: Modal + Select2 zeigt dritte Spalte „Qualitaet" bei der INCI-Auswahl (z.B. „Sonnenblumenoel — bio kaltgepresst")
- Katalog JSON:
ingredient_cataloginProductController::edit()umquality_nameerweitern - Anzeige Rezeptur-Tabelle: Qualitaet als Spalte neben INCI-Name anzeigen
5.1b.2 — Rezeptur: Gramm → Prozent (3 Nachkommastellen) — [x] erledigt
- Migration: Spalte
graminproduct_ingredientsumbenennen zupercentage(decimal 8,3) - Model:
ProductIngredientCast + Fillable anpassen - Frontend: Label „Gramm" → „Anteil (%)", Input mit
step="0.001", 3 Nachkommastellen - Effektiv-Berechnung:
Effektiv = Prozent × Faktor - Summenzeile: Am Ende der Rezeptur-Tabelle eine Zeile „Gesamt" mit Summe aller Prozentwerte
- Wenn Summe == 100.000 → gruen
- Wenn Summe != 100.000 → rot + Hinweis „Die Gesamtrezeptur ergibt nicht 100 %!"
- Backend:
ProductRepository::updateIngredients()+ Validierung anpassen
5.1b.3 — Hersteller-Rezeptur (zweiter Kasten) — [x] erledigt
- Konzept: Zweiter identischer INCI-Kasten im Produktformular fuer die „Hersteller-Rezeptur"
- Migration: Neue Spalte
recipe_typeENUM(product,manufacturer) anproduct_ingredientsmit Defaultproduct - Model: Zwei Scopes am
Product:productIngredients()undmanufacturerIngredients()mit Filter aufrecipe_type - Frontend: Zweite sortierbare Tabelle „Hersteller Rezeptur" unter der Produkt-Rezeptur (gleiche Funktionalitaet: Drag & Drop, Prozent, Faktor, 100%-Summe)
- Backend:
ProductRepository::updateIngredients()verarbeitet beide Rezepturen getrennt (separate Hidden-Feldermanufacturer_inci_sync_sent) - Kopie:
copy()kopiert beide Rezepturen
5.1c — Produktion-Korrekturen
5.1c.1 — Produktion bearbeitbar (edit/update) — [x] erledigt
- Controller:
ProductionController::edit()undupdate()implementieren - FormRequest:
UpdateProductionRequesterstellen - View:
resources/views/admin/inventory/productions/edit.blade.php(analog create, vorbelegt) - Route:
Route::resource('productions', ...)deckt edit/update bereits ab (wenn Standard-Resource) - Hinweis: Bereits abgeschlossene Produktionen editierbar machen (Fehlerkorrektur)
5.1c.2 — Produktion kopierbar — [x] erledigt
- Controller:
ProductionController::copy(Production $production)→ Zeigt create-Formular vorbelegt mit Daten der Quell-Produktion (Produkt, Standort, Chargen-Zuordnungen) - Route:
GET admin/inventory/productions/{production}/copy - View: Nutzt
create.blade.phpmit vorbelegten Werten - Use Case: Gleiche Rezeptur fuer verschiedene Groessen (z.B. Deobalm 50ml → 5ml)
5.1c.3 — Nur aktive Produkte im Dropdown — [x] erledigt
- Controller:
ProductionController::create()+edit():Product::where('active', 1)->get() - API:
recipeJson()auch fuer inaktive zulassen (fuer bestehende Produktionen)
5.1c.4 — Default Lagerort Koeln — [x] erledigt
- Controller:
ProductionController::create(): Defaultlocation_id= Koeln (auslocations-Tabelle) - View: Dropdown vorselektiert
Tests Phase 5.1
- Feature-Test: Menue-Labels korrekt angezeigt (Rohstoffqualitaet etc.)
- Feature-Test: PackagingItem mit URL speichern
- Feature-Test: Lieferant Default-Land = DE
- Feature-Test: INCI mit Qualitaet speichern + Anzeige im Produktformular
- Feature-Test: Rezeptur in Prozent speichern (3 Nachkomma)
- Feature-Test: 100%-Summen-Validierung
- Feature-Test: Hersteller-Rezeptur separat speichern
- Feature-Test: Produktion bearbeiten (edit/update)
- Feature-Test: Produktion kopieren
- Feature-Test: Nur aktive Produkte im Dropdown
Deliverable
Alle Feedback-Punkte der Kundin umgesetzt; Menustruktur, Rezeptur und Produktion entsprechen den Erwartungen.
Phase 6: Bestandsuebersicht & Alarme (4-5 Tage)
Ziel: Vier Bestandsseiten mit Echtzeit-Berechnung und Alarm-System.
Aufgaben
6.1 - Service: InventoryService
- Datei:
app/Services/InventoryService.php - Methoden:
getIngredientStock(?int $locationId = null)- Rohstoff-BestaendegetPackagingStock(string $category, ?int $locationId = null)- Packaging/VersandgetItemsBelowThreshold()- Alle Artikel unter MeldebestandcalculateDynamicThreshold(int $ingredientId)- NEU (Feedback): Automatische Berechnung
- Bestandsberechnung Rohstoffe:
SUM(stock_entries.received_quantity) - SUM(production_ingredients.quantity_used) - SUM(stock_disposals.quantity) - Bestandsberechnung Packaging:
SUM(stock_entries.received_quantity) - SUM(production_packagings.quantity_used) - SUM(stock_disposals.quantity)
6.1.1 - Dynamische Meldebestands-Berechnung (Feedback)
- Algorithmus: Durchschnittlicher Tagesverbrauch = Mittelwert aus:
- Durchschnittsverbrauch der letzten 12 Monate (pro Tag)
- Durchschnittsverbrauch der letzten 3 Monate (pro Tag)
- Daraus ergibt sich ein gewichteter Tagesverbrauch, der Wachstum beruecksichtigt
- Meldebestand = Tagesverbrauch × konfigurierbare Vorlaufzeit (Default: 14 Tage)
- Startphase: Solange keine ausreichende Produktionshistorie (< 3 Monate) vorhanden ist, wird
ingredients.min_stock_alert(manueller Wert) als Fallback verwendet - Umschaltung: Ab Live-Schaltung mit genuegend Daten uebernimmt die Formel automatisch
- Meldebestand-Quelle: Dynamisch berechnet ODER
ingredients.min_stock_alert/packaging_items.min_stock_alertals Fallback
6.2 - Controller
php artisan make:controller Admin/Inventory/StockController --no-interaction
- Methoden:
ingredients(),packaging(),labels(),shippingOffice() - Jede Methode liefert Bestandsdaten pro Lagerort + Gesamt
6.3 - Views
resources/views/admin/inventory/stock/ingredients.blade.phpresources/views/admin/inventory/stock/packaging.blade.phpresources/views/admin/inventory/stock/labels.blade.phpresources/views/admin/inventory/stock/shipping-office.blade.php- Jede View: DataTable mit dynamischen Spalten (Lagerorte aus
locations-Tabelle), Gesamt, Meldebestand, Status - Rote Hervorhebung bei Unterschreitung
- Neue Lagerorte -> automatisch neue Spalten (kein Hardcoding)
6.4 - View Composer: Menue-Badges
- Datei:
app/Http/ViewComposers/InventoryBadgeComposer.php - Registrierung in
AppServiceProvider::boot():View::composer('layouts.includes.layout-sidenav', InventoryBadgeComposer::class); - Zaehlt Artikel unter Meldebestand pro Kategorie
- Ergebnis wird im Sidenav als rote Badges dargestellt
- Performance: Query mit Caching (5 Minuten) um Seitenlade-Performance nicht zu beeintraechtigen
6.5 - Cron-Job: Bestands-Alarm E-Mail
php artisan make:command InventoryAlertCommand --no-interaction
- Registrierung in
app/Console/Kernel.php:->dailyAt('07:00') - Prueft alle Bestaende gegen Meldebestand
- Sendet zusammenfassende E-Mail an konfigurierbare Adresse (Setting:
inventory_alert_email)
6.6 - E-Mail: Mailable
php artisan make:mail InventoryAlertMail --no-interaction
- Blade-Template mit Liste der Artikel unter Meldebestand
Tests
- Unit-Test: InventoryService Bestandsberechnung (Rohstoffe)
- Unit-Test: InventoryService Bestandsberechnung (Packaging via Snapshots)
- Unit-Test: InventoryService Threshold-Berechnung
- Feature-Test: Bestandsseite zeigt korrekte Werte pro Lagerort
- Feature-Test: Dynamische Spalten bei neuem Lagerort
- Feature-Test: Badge-Composer liefert korrekte Zahlen
- Feature-Test: Cron-Job sendet E-Mail bei Unterschreitung
Deliverable
Vier Bestandsseiten mit Live-Berechnung, dynamischen Lager-Spalten, Menue-Badges und taeglichem E-Mail-Alarm.
Phase 7: Ausgang / Ausschuss (2-3 Tage)
Ziel: Entsorgungen und Ausschuss erfassen.
Aufgaben
7.1 - Model & Migration
php artisan make:model StockDisposal -mf --no-interaction
- Inkl.
unitENUM('gram','piece') Spalte
7.2 - Repository
StockDisposalRepository- CRUD
7.3 - Controller & FormRequest
php artisan make:controller Admin/Inventory/StockDisposalController --resource --no-interaction
7.4 - Views
index.blade.php- DataTable mit allen Entsorgungen (filterbar nach Typ + Zeitraum)create.blade.php- Formular: Typ, Artikel (dynamisch), Charge (optional), Lagerort, Menge, Grund, Datumunitwird automatisch gesetzt je nach Typ
7.5 - Integration in Bestandsberechnung
InventoryServiceberuecksichtigtstock_disposalsbereits in der Formel (Phase 6)- Sicherstellen, dass Ausgang korrekt abgezogen wird
Tests
- Feature-Test: Ausgang erstellen
- Feature-Test: Bestand wird nach Ausgang korrekt reduziert
- Feature-Test: unit wird korrekt gesetzt
- Feature-Test: Grund ist Pflichtfeld
Deliverable
Vollstaendige Ausschuss-Verwaltung mit Auswirkung auf Bestaende.
Phase 8: Rollen, Audit-Trail & Feinschliff (3-4 Tage)
Ziel: Audit-Trail, Feinschliff Berechtigungen, Code-Qualitaet.
Aufgaben
8.1 - Migration: inventory_logs
php artisan make:model InventoryLog -m --no-interaction
- Polymorphe Struktur:
loggable_type,loggable_id,action,user_id,changes(JSON)
8.2 - Observer: Automatisches Logging
- Dateien:
app/Observers/StockEntryObserver.php,ProductionObserver.php,StockDisposalObserver.php - Registrierung in
AppServiceProvider::boot() - Bei
created,updated: Eintrag ininventory_logsmit alten/neuen Werten
8.3 - Berechtigungs-Feinschliff
- Preis-Felder (
price_per_kg,price_total) nur fuer admin >= 7 sichtbar - Stammdaten-Bearbeitung nur fuer SuperAdmin
- Gates oder Policy-Klassen nach Bedarf
- Ueberpruefung aller Controller auf korrekte Middleware/Autorisierung
8.4 - Code-Qualitaet
vendor/bin/pint --dirtyauf alle geaenderten Dateien- Alle Feature-Tests durchlaufen lassen
- Review aller Views auf konsistentes UI (Bootstrap 4)
8.5 - Menue-Integration: Finalisierung
- Alle Menuepunkte mit korrekten Badges
- Korrekte
active-Klassen fuer Routing - Uebersetzungen in
lang/navigation.php
8.6 - Doku: Inline-Kommentare & PHPDoc
- PHPDoc-Bloecke fuer alle neuen Services, Repositories, Models
- Array-Shape-Definitionen fuer komplexe Rueckgabewerte
Tests
- Feature-Test: Audit-Log wird bei jeder Bestandsveraenderung erstellt
- Feature-Test: CopyReader sieht keine Preise
- Feature-Test: SuperAdmin kann alle Stammdaten verwalten
- Alle bestehenden Tests laufen weiterhin gruen
Deliverable
Produktionsreifes System mit Audit-Trail, sauberer Rechtevergabe und vollstaendiger Test-Abdeckung.
Phase 9: Einstellungen & Konfiguration (1-2 Tage)
Ziel: Konfigurierbare Warenwirtschafts-Einstellungen ueber bestehendes Setting-System.
Aufgaben
9.1 - Controller
php artisan make:controller Admin/Inventory/InventorySettingController --no-interaction
- Analog zum bestehenden
SettingController(app/Http/Controllers/SettingController.php) - Nutzt
Setting::getContentBySlug()/Setting::setContentBySlug()
9.2 - View
resources/views/admin/inventory/settings.blade.php- Felder:
- E-Mail-Adresse fuer Alarme (Text, Default:
service@gruene-seele.bio) - Alarm aktiv? (Bool, Default: true)
- Standard-Lagerort (Dropdown aus locations, Default: erster Lagerort)
- E-Mail-Adresse fuer Alarme (Text, Default:
9.3 - Routes
Route::middleware(['auth:user', 'superadmin'])->prefix('admin/inventory')->group(function () {
Route::get('settings', [InventorySettingController::class, 'index'])->name('inventory.settings');
Route::post('settings', [InventorySettingController::class, 'store'])->name('inventory.settings.store');
});
9.4 - Integration
InventoryAlertCommandliest E-Mail ausSetting::getContentBySlug('inventory_alert_email')StockEntryControllernutztSetting::getContentBySlug('inventory_default_location')als Default
Tests
- Feature-Test: Settings speichern und laden
- Feature-Test: Nur SuperAdmin hat Zugriff
Deliverable
Konfigurierbare Einstellungen fuer die Warenwirtschaft.
Zusammenfassung: Dateien & Artefakte
Neue Models (13)
| Model | Tabelle | Phase |
|---|---|---|
| Location | locations | 2 |
| SupplierCategory | supplier_categories | 2 |
| Supplier | suppliers | 2 |
| MaterialQuality | material_qualities | 2 |
| PackagingMaterial | packaging_materials | 2 |
| PackagingItem | packaging_items | 2 |
| StockEntry | stock_entries | 3 |
| ProductPackaging | product_packagings | 4 |
| Production | productions | 5 |
| ProductionIngredient | production_ingredients | 5 |
| ProductionPackaging | production_packagings | 5 |
| StockDisposal | stock_disposals | 7 |
| InventoryLog | inventory_logs | 8 |
Plus Pivot-Tabelle: supplier_supplier_category (kein eigenes Model noetig)
Geaenderte Models (3)
| Model | Aenderung | Phase |
|---|---|---|
| Product | + shelf_life_type, shelf_life_months, Relationen | 1 + 4 |
| ProductIngredient | + pos, gram, factor | 1 |
| Ingredient | + default_factor, min_stock_alert | 1 |
Neue Controller (11)
Alle unter app/Http/Controllers/Admin/Inventory/:
| Controller | Phase |
|---|---|
| LocationController | 2 |
| SupplierCategoryController | 2 |
| SupplierController | 2 |
| MaterialQualityController | 2 |
| PackagingMaterialController | 2 |
| PackagingItemController | 2 |
| StockEntryController | 3 |
| ProductionController | 5 |
| StockController | 6 |
| StockDisposalController | 7 |
| InventorySettingController | 9 |
Neue Repositories (5)
| Repository | Phase |
|---|---|
| SupplierRepository | 2 |
| PackagingItemRepository | 2 |
| StockEntryRepository | 3 |
| ProductionRepository | 5 |
| StockDisposalRepository | 7 |
Neue Services (2)
| Service | Phase |
|---|---|
app/Services/ProductionService.php |
5 |
app/Services/InventoryService.php |
6 |
Neue Views (~27)
Alle unter resources/views/admin/inventory/:
- Stammdaten: je
index.blade.php+form.blade.php(12 Views) - Eingang: index, create, show (3 Views)
- Produktion: index, create, show (3 Views)
- Bestand: 4 Views (je Kategorie)
- Ausgang: index, create (2 Views)
- Einstellungen: 1 View
- Angepasste Views:
admin/product/form.blade.php(INCI + Packaging)
Neue Artisan Commands (1)
app/Console/Commands/InventoryAlertCommand.php
Tests (Phase 0 & 1 – umgesetzt)
tests/Feature/ProductPhase0Test.php– Relationen, Legacy-Syncproduct_ingredients, Dropdown-Sortierung, Attribut-Kopietests/Feature/ProductPhase1Test.php– Pivot Gramm/Faktor/Haltbarkeit, Produktkopie INCI-Pivot
Geaenderte Dateien
routes/web.php- Neue Route-Gruppenresources/views/layouts/includes/layout-sidenav.blade.php- Menuepunkt + Badgesresources/views/admin/product/form.blade.php- INCI-Tabelle, Warenwirtschaft Haltbarkeit (Phase 1); Packaging spaeter Phase 4resources/views/admin/product/edit.blade.php- SortableJS + INCI-JavaScript (Phase 1)resources/views/admin/ingredient/form.blade.php-default_factor,min_stock_alert(Phase 1)app/Http/Controllers/ProductController.php-ingredient_catalogfuer Produkt-Edit (Phase 1)app/Http/Controllers/IngredientController.php- Speichern der neuen Ingredient-Felder (Phase 1)app/Repositories/ProductRepository.php- Bugfixes + Erweiterungenapp/Services/HTMLHelper.php- Sortierung INCI-Dropdown (Phase 0)app/Providers/AppServiceProvider.php- View Composer + Observerapp/Console/Kernel.php- Neuer Cron-Jobapp/Models/Product.php- Relationen + Bugfixapp/Models/Ingredient.php- Neue Felder + Bugfix (ab Phase 1); Phase 0: nur hasMany-Fixapp/Models/ProductIngredient.php- Neue Felder (Phase 1)lang/de/navigation.php(o.ae.) - Uebersetzungendatabase/migrations/2018_09_29_145909_create_countries_table.php- Phase 0: doppelte Spalteactiveentferntdatabase/migrations/2021_06_15_112357_create_product_buys_table.php- Phase 0: FKauth_user_idkorrigiertdatabase/migrations/2026_03_27_111352_add_recipe_fields_to_product_ingredients_table.php- Phase 1database/migrations/2026_03_27_111353_add_shelf_life_to_products_table.php- Phase 1database/migrations/2026_03_27_111354_add_inventory_fields_to_ingredients_table.php- Phase 1
Risiken & Empfehlungen
| Risiko | Mitigation |
|---|---|
| Performance bei Bestandsberechnung (viele Summen-Queries) | Caching der Badge-Counts (5 Min), InventoryService mit optimierten Aggregat-Queries |
| Komplexitaet Chargen-Splitting UI | JS-Prototyp frueh bauen, mit Kundin testen |
| Multi-Lager erweitert sich auf > 2 Standorte | Datenmodell + Views sind bereits dynamisch (n Lagerorte) |
| BOM-Aenderung nach Produktion | Geloest durch production_packagings Snapshot-Tabelle |
| LUCID-Report nicht im MVP | Als Phase 10 planbar, Datengrundlage ist vorhanden |
| Bestehende Tests brechen durch Migrations | Tests nutzen SQLite in-memory - Migrations laufen automatisch |
Empfohlene Reihenfolge fuer sofortigen Nutzen
- Phase 0 + 1 zuerst - Behebt Bugs und macht die INCI-Verwaltung sofort besser (Quick Wins)
- Phase 2 parallel beginnen - Stammdaten sind unabhaengig
- Phase 3-5 sequentiell - bilden den Kern der Warenwirtschaft
- Phase 6 liefert den groessten Wow-Effekt (Bestandsuebersicht mit Badges)
- Phase 7 + 8 + 9 zum Abschluss - Ausschuss + Audit-Trail + Settings runden ab