# 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` (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 `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` (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 ```bash 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 ```bash 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 ```bash 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`: ```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 - [x] `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 ```bash 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 ```bash 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 ```php 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 - [x] Feature-Test: Stufe 1 - Einkauf erstellen (Status pending) — `InventoryPhase3Test.php` - [x] Feature-Test: Stufe 2 - Wareneingang buchen (Status received) - [x] Feature-Test: unit wird korrekt gesetzt (gram/piece) - [x] Feature-Test: CopyReader kann Stufe 2, aber nicht Stufe 1 (Create-Form Redirect) - [x] Feature-Test: Admin kann Stufe 1 (store) - [x] 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 ```bash 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 - [x] Feature-Test: Packaging am Produkt zuweisen und speichern — `ProductPhase4Test.php` - [x] Feature-Test: Produkt-Kopie uebernimmt Packaging - [x] 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 ```bash 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 ```bash php artisan make:controller Admin/Inventory/ProductionController --resource --no-interaction ``` - `StoreProductionRequest` - Validierung aller Felder inkl. Chargen-Zuordnung #### 5.4 - Routes ```php 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 - [x] Feature-Test: Produktion erstellen mit Chargen-Zuordnung — `ProductionPhase5Test.php` - [ ] Feature-Test: Bestandsabzug nach Produktion korrekt berechnet (Rohstoffe) — Phase 6 / `InventoryService` - [x] Feature-Test: Packaging-Snapshot wird korrekt erstellt - [ ] Feature-Test: Chargen-Splitting (2 Chargen fuer 1 INCI) — UI unterstuetzt „Weitere Charge“; separater Assertions-Test optional - [x] 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 - [x] 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 ```bash 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()`: ```php 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 ```bash 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 ```bash 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 ```bash php artisan make:model StockDisposal -mf --no-interaction ``` - Inkl. `unit` ENUM('gram','piece') Spalte #### 7.2 - Repository - `StockDisposalRepository` - CRUD #### 7.3 - Controller & FormRequest ```bash 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 ```bash 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 ```bash 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 ```php 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