gruene-seele/dev/product management /entwicklungsplan.md

43 KiB
Raw Blame History

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.
69 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. | || | 69 | 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 (Klasse HTMLHelper liegt im Projekt unter Services, nicht unter Libraries)
  • Ist-Umsetzung: Ingredient::where('active', 1)->orderBy('name')->get() in getProductIngredientsOptions().

0.4 - Bugfix: Attribute-Kopierung — [x] erledigt

  • Datei: app/Repositories/ProductRepository.php (Methode copy())
  • Ist-Umsetzung: type_id aus der Quell-Zeile ProductAttribute: $attribute->type_id (statt nicht existierendem Feld am Product).

0.5 - Migrationskorrekturen (Tests / Schema-Sauberkeit) — [x] erledigt

Nicht im urspruenglichen Plan; noetig damit php artisan test mit 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 Productproduct_ingredients Test: korrekte FK-Verknuepfung
Relation Ingredientproduct_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 (ENUM pao/fixed, nullable), shelf_life_months (nullable)

1.4 - Model: ProductIngredient aktualisieren — [x] erledigt

  • fillable + casts fuer pos, gram, factor
  • Product::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 in IngredientController::store() (Zahlen mit reFormatNumber)

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 aus ProductController::edit() als ingredient_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 Arrays pi_ingredient_id, pi_gram, pi_factor; sync() mit Pivot; Hidden product_inci_sync_sent: verhindert, dass Speichern ohne INCI-Bereich alle INCIs loescht; Legacy-Pfad product_ingredients[] (ohne Hidden) fuer Tests/Altkunden
  • update(): Normalisierung shelf_life_type / shelf_life_months
  • copy(): kopiert pos, gram, factor sowie Haltbarkeit (via replicate())

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.php innerhalb der bestehenden auth:user-Gruppe, verschachtelt mit superadmin bzw. admin (analog zum Plan, nur ohne separaten aeusseren auth: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.md Abschnitt 3
  • Factories mit sinnvollen Faker-Daten
  • Pivot-Tabelle supplier_supplier_category fuer n:m Lieferant-Kategorie-Beziehung
  • Supplier: belongsToMany(SupplierCategory::class) statt belongsTo

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 StoreRequest und UpdateRequest FormRequest-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 kein locations.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. unit ENUM('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 auf admin beschraenkt, 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_packagings mit pos; 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 fuer product_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-Eintraege
  • ProductionService - Orchestriert den Produktions-Workflow:
    1. production_ingredients-Eintraege erstellen
    2. production_packagings-Snapshot erstellen
    3. 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.md umsetzen, 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)
  • Migration: ENUM aendern auf packaging|shipping; bestehende label und shipping_office auf shipping migrieren
  • 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) an packaging_items anfuegen
  • Model: url in $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_id beim Erstellen: ID von Deutschland (Country::where('code', 'DE')->first()->id)
  • Controller create(): $defaultCountryId mitgeben

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_id am 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) an ingredients-Tabelle
  • Model: Ingredient erhaelt belongsTo(MaterialQuality::class) Relation; material_quality_id in $fillable
  • Formular: resources/views/admin/ingredient/form.blade.php — neues Select2-Dropdown „Rohstoffqualitaet" (Daten aus material_qualities)
  • Produktformular: Modal + Select2 zeigt dritte Spalte „Qualitaet" bei der INCI-Auswahl (z.B. „Sonnenblumenoel — bio kaltgepresst")
  • Katalog JSON: ingredient_catalog in ProductController::edit() um quality_name erweitern
  • Anzeige Rezeptur-Tabelle: Qualitaet als Spalte neben INCI-Name anzeigen

5.1b.2 — Rezeptur: Gramm → Prozent (3 Nachkommastellen) — [x] erledigt

  • Migration: Spalte gram in product_ingredients umbenennen zu percentage (decimal 8,3)
  • Model: ProductIngredient Cast + 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_type ENUM(product, manufacturer) an product_ingredients mit Default product
  • Model: Zwei Scopes am Product: productIngredients() und manufacturerIngredients() mit Filter auf recipe_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-Felder manufacturer_inci_sync_sent)
  • Kopie: copy() kopiert beide Rezepturen

5.1c — Produktion-Korrekturen

5.1c.1 — Produktion bearbeitbar (edit/update) — [x] erledigt

  • Controller: ProductionController::edit() und update() implementieren
  • FormRequest: UpdateProductionRequest erstellen
  • 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.php mit 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(): Default location_id = Koeln (aus locations-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-Bestaende
    • getPackagingStock(string $category, ?int $locationId = null) - Packaging/Versand
    • getItemsBelowThreshold() - Alle Artikel unter Meldebestand
    • calculateDynamicThreshold(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:
    1. Durchschnittsverbrauch der letzten 12 Monate (pro Tag)
    2. 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_alert als 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.php
  • resources/views/admin/inventory/stock/packaging.blade.php
  • resources/views/admin/inventory/stock/labels.blade.php
  • resources/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. unit ENUM('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, Datum
  • unit wird automatisch gesetzt je nach Typ

7.5 - Integration in Bestandsberechnung

  • InventoryService beruecksichtigt stock_disposals bereits 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 in inventory_logs mit 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 --dirty auf 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)

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

  • InventoryAlertCommand liest E-Mail aus Setting::getContentBySlug('inventory_alert_email')
  • StockEntryController nutzt Setting::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-Sync product_ingredients, Dropdown-Sortierung, Attribut-Kopie
  • tests/Feature/ProductPhase1Test.php Pivot Gramm/Faktor/Haltbarkeit, Produktkopie INCI-Pivot

Geaenderte Dateien

  • routes/web.php - Neue Route-Gruppen
  • resources/views/layouts/includes/layout-sidenav.blade.php - Menuepunkt + Badges
  • resources/views/admin/product/form.blade.php - INCI-Tabelle, Warenwirtschaft Haltbarkeit (Phase 1); Packaging spaeter Phase 4
  • resources/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_catalog fuer Produkt-Edit (Phase 1)
  • app/Http/Controllers/IngredientController.php - Speichern der neuen Ingredient-Felder (Phase 1)
  • app/Repositories/ProductRepository.php - Bugfixes + Erweiterungen
  • app/Services/HTMLHelper.php - Sortierung INCI-Dropdown (Phase 0)
  • app/Providers/AppServiceProvider.php - View Composer + Observer
  • app/Console/Kernel.php - Neuer Cron-Job
  • app/Models/Product.php - Relationen + Bugfix
  • app/Models/Ingredient.php - Neue Felder + Bugfix (ab Phase 1); Phase 0: nur hasMany-Fix
  • app/Models/ProductIngredient.php - Neue Felder (Phase 1)
  • lang/de/navigation.php (o.ae.) - Uebersetzungen
  • database/migrations/2018_09_29_145909_create_countries_table.php - Phase 0: doppelte Spalte active entfernt
  • database/migrations/2021_06_15_112357_create_product_buys_table.php - Phase 0: FK auth_user_id korrigiert
  • database/migrations/2026_03_27_111352_add_recipe_fields_to_product_ingredients_table.php - Phase 1
  • database/migrations/2026_03_27_111353_add_shelf_life_to_products_table.php - Phase 1
  • database/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

  1. Phase 0 + 1 zuerst - Behebt Bugs und macht die INCI-Verwaltung sofort besser (Quick Wins)
  2. Phase 2 parallel beginnen - Stammdaten sind unabhaengig
  3. Phase 3-5 sequentiell - bilden den Kern der Warenwirtschaft
  4. Phase 6 liefert den groessten Wow-Effekt (Bestandsuebersicht mit Badges)
  5. Phase 7 + 8 + 9 zum Abschluss - Ausschuss + Audit-Trail + Settings runden ab