April 2026 waren Wirtschaft Feedback

This commit is contained in:
Kevin Adametz 2026-04-10 17:14:38 +02:00
parent 02f2a4c23e
commit 9ce711d6b2
167 changed files with 25278 additions and 8518 deletions

View file

@ -0,0 +1,139 @@
A) Vorab: INCI-Liste bei Produkten userfreundlich machen
a) die INCI kopieren sich bei Produktkopie nicht mit
b) Ich kann die INCI nicht sortieren - hier entweder drag & drop Funktion oder Pfeil nach oben / unten (das aber live und nicht bei jedem Verschieben neu abspeichern; -> INICs mit Pfeilen hoch/runter sortieren, wenn alles in der richtigen Reihenfolge ist, dann speicher ich ab).
c) Dropdown für Mehrfachauswahl muss 1. alphabetisch geordnet sein und die INCI dann so übernommen werden in der Reihenfolge wie ich sie angeklickt habe.
B) „Warenwirtschaft" anlegen
Bitte neuen Hauptmenüpunkt anlegen: Warenwirtschaft (gleiche Hierarchie wie Produkte, nur in der Reihenfolge darunter).
Unter-Menüpunkt: Lieferanten
Unter-Menüpunkt: Lieferanten Kategorien
Bei dem zweiten Menüpunkt möchte ich dann ganz einfach neue Kategorien für Lieferanten anlegen (z. B. Rohstoffe, Versandmaterial, Kosmetiktiegel, Verpackungen etc.). Speichern.
Bei dem ersten Menüpunkt kommt dann die Liste mit den Lieferanten, die man anlegt (inkl. Suchfeld oben rechts). Dann „+ Neuen Lieferanten anlegen“ mit folgenden Feldern:
Dropdown: Art (z. B. Rohstoffe, Versandmaterial, Verpackungen, Kosmetiktiegel etc.)
Textfeld: Name der Firma
Textfeld: URL (optional)
Textfeld: Ansprechpartner
Textfeld: Email
Textfeld: Telefonnummer
Dropdown: Land (Default Deutschland)
Textfeld: Notizen
C) Eintragen von Rezepturen (Rohstoff-Ausgang)
Damit wir wissen, wieviel Rohstoffe wir bei der Produktion verbraucht haben, muss ich bei jedem einzelnen Produkt die genaue Mengenangabe hinterlegen.
D.h. bei den INCIs in jedem Einzelprodukt muss ich noch ein Feld für die Grammzahl eintragen. Wir rechnen nur in Gramm, auch wenn Öl in ml / Liter geliefert wird. Das macht es einfacher in der Rezeptur. Ich muss z. B. Öle aber anders gewichten. Öl ist leichter als Wasser z.B. (90% vom Wassergewicht). Darüber hinaus haben wir „Verschleiß“ bei der Produktion. Bei jedem Rezept habe ich mir in Excel noch einen Faktor gebaut (z. B. 1,1 oder 1,2), mit dem ich dann entsprechend 10 oder 20% mehr einrechne als in der Rezeptur steht. Nur so komme ich auf die geplante Stückzahl. So ein Faktor-Textfeld (Default 1,1 - aber editierbar) brauche ich auch unter der INCI-Liste, dann haben wir den tatsächlichen Verbrauch.
Außerdem muss ich die Haltbarkeit hinterlegen. Ich kann den 12M-Tiegel einsetzen, das bedeutet „12 Monate nach Öffnen“. Dann brauche ich nichts weiter eintragen bei einer Produktion. Dann gibt es Produkte, wo ich ein konkretes MHD eintrage (6, 12, 18, 24, 30 oder 36 Monate). Vielleicht sollten wir das mit Radio-Buttons lösen:
a) 12M-Tiegel
b) Auswahl der Anzahl der Monate
D) Rohstoff-Eingang
Ich habe z.B. 25 kg Sheabutter gekauft und die ist eingetroffen. Dann geht man auf Menüpunkt „Eingang". Man sieht eine Liste aller Einträge von Eingängen. Nun klickt sie auf „+ Neuer Eingang“. Dann kommt als erstes ein Dropdown, in dem ich die Kategorie auswähle (Rohstoff, Versand & Büro etc.). Je nachdem, was ich auswähle, bekomme ich individuelle Felder. Fangen wir erstmal mit den Rohstoffen an. Die Felder definieren sich wie folgt:
Felder für Einkäufer (das bin ICH):
Dropdown: Lieferant
Suchfeld : Inhaltsstoff auswählen über Textfeld mit Autosuggest, da wir bald über 200 Rohstoffe haben
Kalender: Datum „Einkauf am“ (aktuelles Datum vorbelegt)
Textfeld: Menge in Gramm
Preisfeld: Netto-Preis / KG (00,00 €)
Textfeld: Alarm, wenn definierte Menge unterschritten wird (Mail an service@gruene-seele.bio) -> Hier trage ich z. B. 5.000 g ein für Sheabutter
Das habe ich ausgefüllt, sobald ich bestellt habe. Wenn die Ware eintrifft, geht Ivonne ins Partnercenter unter Eingang und sieht meine „Bestellung“ auf Pending (oranger Punkt vorne). Geht da rein und füllt folgende Felder aus:
Felder für Eingang (das macht IVONNE):
Kalender: Datum „Eingang am“
Textfeld: Menge in Gramm
Textfeld: Chargen-Nr.
Kalender: für MHD
Da die Ware eingegangen ist, wird der Punkte vorne grün und sortiert sich ein. Vorbelegte Sortierlogik: 1. nach Lieferstatus (Pendings stehen oben), 2. nach Datum (Pendings sind nach Datum sortiert und eingetroffene Ware (grüner Punkt) auch.
Die Felder für die anderen Kategorien (Verpackung etc.) folgen später. Das sprengt hier sonst den Rahmen.
E) Produktion
Ivonne produziert 500 Tattoocremes. Sie geht auf den Menüpunkt „Produktion“ und sieht eine Liste mit vergangenen Produktionseinträgen. Oben der Button „+ Neue Produktion eintragen“. Klick: Dropdown erscheint zur Auswahl des Produktes (z. B. Tattoocreme 40 ml). Weitere Felder, die Sie ausfüllen muss:
Kalender: Datum „Produziert am“ (vorbelegt mit heutigem Tag)
Textfeld: Anzahl
Liste: Mit allen INCI, die für das Produkt verwendet wurden. Hinter jedem INCI ein Dropdown mit den letzten 3 Chargen-Eingängen (die aktuellsten stehen oben) -> Mehrfachauswahl möglich
F) Bestand
Man könnte eine Seite mit „Bestand“ aller Materialen machen und dann filtern. Aus Gründen der schnellen Übersicht, will ich das aber nicht. Ich möchte für jeden Kategorietyp einen eigenen Menüpunkt mit einem Alarmhinweis (z.B. roter Kreis mit Zahl drin am Menüpunkt) - dann sehe ich gleich am Menü, das irgendwo was fehlt.
Also auch unter Warenwirtschaft kommen dann folgende Menüpunkte:
Bestand Rohstoffe (2) -> Wenn hier an Menüpunkt kleiner roter Kreis mit 2 auftaucht -> zwei Rohstoffe unter die Grenze gerutscht: nachbestellen!
Bestand Tiegel
Bestand Verpackungen
Bestand Versand & Office
G) Ausgang
Rohstoffe werden schlecht, Verpackungen veralten, wir müssen leider auch viel entsorgen. Dafür würde ich gerne eine entsprechende „Schrottliste“ führen. Wichtig ist hier:
Was
Wieviel
Warum (Kommentarfeld)
Wer (Name ermitteln durch Account, wer ist eingeloggt)
Datum (Kalender aktueller Tag vorbelegt)
…………………………….
Übersicht Menü-Block „Warenwirtschaft“:
Eingang
Ausgang
Produktion
Lieferanten
Lieferanten Kategorien
Bestand Rohstoffe
Bestand Tiegel
Bestand Verpackungen
Bestand Versand & Office
Zu bedenken:
1. Ivonne darf nur Rechte „Redakteur“ haben und dementsprechend auch nicht mehr anklicken können. Weiß nicht, ob die Rechtevergabe schon funktioniert.
2. Alle Einträge sollten geloggt werden durch den Account, mit dem jemand eingeloggt ist.
Bitter ergänzen bei Rohstoff-Eingang:
Qualität / Sorte
-> hier wähle ich aus:
- konventionell
- bio kaltgepresst
- bio raffiniert
- konventionell kaltgepresst
- konventionell raffiniert
Diese Parameter muss ich also vorher auch noch anlegen können.
Außerdem:
Solange wir über 2 Orte (Köln und Waldbröl) arbeiten, brauche ich ein Dropdown, wo ich vermerken kann, wo die Ware eingegangen ist, also wo das gerade lagert. Dann kann man immer sehen, wo noch was liegt an Reserve.
Bitte ergänzen bei Produkt, wo wir Materialen anlegen:
Material
Gewicht
Beispiel: Pechsalbe
Glastiegel 50 ml
Bambusdeckel
Umverpackung
Flyer
Zu jeder Position muss ich Material und Gewicht eintragen können. Brauche ich für die Abfallwirtschaft. Da bezahle ich nach Material und Gewicht. Material sollte mal also vorher auch als Kategorie anlegen können, Gewicht wieder in Gramm tragen ich dann in ein Feld ein.

View file

@ -0,0 +1,878 @@
# 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 `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

View file

@ -0,0 +1,36 @@
Produkt:
1. Inhaltsstoffe Rezeptur - hier Feld in Prozent umstellen (Ganz wichtig, wenn wir jetzt von der Milligramm auf Prozent gehen. Auf drei Nachkommastellen müssen auch alle weiteren Felder und Berechnungen das berücksichtigen. Das ist der Ausgangswert, der ja die Rezeptur beinhaltet.)
Zusätzlich soll bei der Rezeptur am Ende immer die Gesamtprozentzahl stehen d.h. es muss am Ende auf 100 % aufgehen. Ansonsten muss die Zahl rot sein und ein Hinweis dort stehen!
2. Weiterer Kasten „Hersteller Rezeptur“ - gleiches Spiel, aber hier lege ich für mich andere INCI-Produkte an
Menüstruktur:
- Materialqualität umbennen in Rohstoffqualität
- Verpackungsmaterialien umbenennen in Verpackungsmaterial (Singular)
- Sollte der Menüpunkt Stammdaten nicht besser Lieferanten heißen? Und der erste darunter dann nur Kategorien?
- Verpackungsartikel umbenennen in „Produktverpackung“
- Weiteren Menüpunkt mit „Versandverpackung“
- Lieferant anlegen (Deutschland als Default bei Land)
Produktion
- Produktionen müssen bearbeitet werden können, falls einem ein Fehler auffällt.
- Bitte eine Produktion kopierbar machen. Wir produzieren in einem Gang z.B. Deobalm 50 ml und 5 ml. Die muss ich ja hier einzeln anlegen. Wenn ich bei beiden zweimal für die INCI die Chargen-Nr. zuordnen muss, wäre das ganz schön nervig.
- Bei Anlegen einer neuen Produktion sollten nur „aktive Produkte“ angezeigt werden
- Lagerort bitte Default Köln einstellen
Berechnung von Lagerrückstand / Rohstoffknappheit
Nur einen Wert einzutragen, ab wann wieder bestellt werden muss, ist mir zu profan. Ich möchte, dass das System das berechnet mit Parametern wie „durchschnittlicher Verbrauch eines Rohstoffs (z. B. Sonnenblumenöl) pro Tag der letzten 12 Monate“ und „durchschnittlicher Verbrauch der letzten 3 Monate. Daraus ziehen wir den Durchschnittswert. Was hier rauskommt ist der durchschnittliche Verbrauch pro Tag. So habe ich nämlich auch einen Wachstumsfaktor mit einbezogen und bekomme realistischere Werte. Vor allem, wenn die Agentur gut performt und wir hoffentlich künftig immer ein merkbares Wachstum haben.
Ich weiß, dass wir das am Anfang noch nicht berechnen können, weil die Historie fehlt. Aber genau dafür würde ich dann das Feld nehmen, das Du bei den INCI hinterlegt hast. D.h. ich trage hier selber den Verbrauch pro Tag ein, den ich mir errechne. Das wäre die Startgrundlage. Ab Live-Schaltung übernimmt die Formel die Berechnung.
#Verpackungsartikel anlegen:
- hier sind Wörter drin, die da nicht reingehören (Etikett oder Versand/Büro etc.)
- die, die ich angelegt habe, kann ich dann noch nicht auswählen (z. B. Verpackungsmaterial -> Lotionpumpe schwarz -> Art)
- die Zuordnung z. B. bei der Lotionpumpe unten über das Dropdown ist optional. Wenn wir das nicht brauchen, raus damit. Wenn wir das brauchen sollten, bitte Mehrfachauswahl
- auch bei Verpackungsartikel benötige ich ein Feld für die URL, dann kann ich da direkt ins Produkt verlinken für den Onlinekauf und man muss nicht suchen
#INCI
- Bei Anlegen eines INCI (z.B. Sonnenblumenöl), muss ich auch die Warenqualität (bio, konventionell etc.) auswählen können. Die habe ich ja bereits unter „Einstellungen“ angelegt. Diese muss dann auch bei der Auswahl des INCIs bei Anlegen einer Rezeptur auf Produktebene sichtbar sein, damit ich das richtige auswähle. Ich nutze z. B. für die Tattoocreme „Sonnenblumenöl bio kaltgepresst“, für den Deobalm aber „Sonnenblumenöl bio raffiniert“. Also brauchen wir hier im Auswahlfenster, das bei „Inhaltsstoffe hinzufügen“ erscheint noch eine dritte Spalte rechts mit der „Qualität“.

View file

@ -0,0 +1,710 @@
# Finales technisches Konzept: Warenwirtschaft & Produktion
> **Version:** 1.1 - Stand 12.03.2026
> **Grundlage:** Kunden-Briefing + initiales Konzept (`briefing.md`, `konzept.md`)
> **System:** Laravel 11 (L10-Struktur), PHP 8.4, Bootstrap 4, jQuery, Laravel Mix, MySQL
---
## Inhaltsverzeichnis
1. [Ist-Analyse & identifizierte Probleme](#1-ist-analyse)
2. [Architektur-Entscheidungen](#2-architektur-entscheidungen)
3. [Datenmodell](#3-datenmodell)
4. [Modul A: INCI-Management (Bugfix & Erweiterung)](#4-modul-a-inci-management)
5. [Modul B: Stammdaten](#5-modul-b-stammdaten)
6. [Modul C: Wareneingang (Zwei-Stufen-System)](#6-modul-c-wareneingang)
7. [Modul D: Rezeptur & BOM (Bill of Materials)](#7-modul-d-rezeptur--bom)
8. [Modul E: Produktion & Bestandsabzug](#8-modul-e-produktion)
9. [Modul F: Bestandsuebersicht & Alarme](#9-modul-f-bestandsuebersicht)
10. [Modul G: Ausgang / Ausschuss](#10-modul-g-ausgang)
11. [Modul H: Rollen, Berechtigungen & Audit-Trail](#11-modul-h-rollen)
12. [Modul I: Warenwirtschafts-Einstellungen](#12-modul-i-einstellungen)
13. [Menuestruktur](#13-menuestruktur)
14. [Abweichungen vom initialen Konzept](#14-abweichungen)
---
## 1. Ist-Analyse
### Bestehende Strukturen
| Bereich | Status | Details |
|---------|--------|---------|
| Produkte | vorhanden | `products`-Tabelle mit umfangreichen Feldern |
| INCI/Inhaltsstoffe | rudimentaer | `ingredients` + `product_ingredients` Pivot (ohne Sortierung, Gramm, Faktor) |
| Warenwirtschaft | nicht vorhanden | Keine Tabellen, keine Routen, kein Menuepunkt |
| Lieferanten | nicht vorhanden | - |
| Lagerorte | nicht vorhanden | - |
| Bestandsfuehrung | nicht vorhanden | - |
| Settings-System | vorhanden | `Setting`-Model mit Key-Value-Store (`getContentBySlug` / `setContentBySlug`) |
### Identifizierte Bugs im Bestand
1. **`ProductIngredient` hasMany-Relation:** Falscher zweiter Parameter (`'product_ingredients'` statt `'product_id'`). Muss korrigiert werden.
2. **`updateIngredients()`:** Fuegt nur neue INCIs hinzu, entfernt deselektierte nicht. Sync-Logik fehlt.
3. **`HTMLHelper::getProductIngredientsOptions()`:** Kein `orderBy('name')` - Dropdown nicht alphabetisch.
4. **Produkt-Kopierung:** INCI-Reihenfolge nicht deterministisch, da kein `orderBy` auf Pivot.
5. **Attribute-Kopierung:** `$this->model->attribute_type_id` existiert nicht auf Product-Model.
---
## 2. Architektur-Entscheidungen
### Frontend-Stack: jQuery + bestehende Libraries
**Begruendung:** Das Projekt nutzt durchgehend jQuery + Bootstrap 4 + Laravel Mix. Die Einfuehrung von Livewire waere ein Architekturbruch und wuerde die Komplexitaet ohne Mehrwert erhoehen.
| Anforderung | Loesung |
|-------------|--------|
| Drag & Drop Sortierung | **SortableJS** (vanilla JS, kein jQuery noetig) |
| Autosuggest / Dropdown | **Select2** (bereits im Projekt-Oekosystem Bootstrap 4 kompatibel) |
| AJAX-Operationen | jQuery `$.ajax` / `$.post` (konsistent mit Bestand) |
| DataTables | **jQuery DataTables** (bereits im Projekt verwendet) |
### Backend-Pattern: Repositories + Services
**Grundregel:** Konsistent mit der bestehenden Architektur (`ProductRepository`, `InvoiceRepository` etc.).
| Pattern | Einsatz | Beispiele |
|---------|---------|-----------|
| **Repositories** | Datenbank-Zugriff und CRUD-Logik, direkt von Controllern verwendet | `SupplierRepository`, `StockEntryRepository`, `ProductionRepository` |
| **Services** | Uebergreifende Business-Logik, Berechnungen, Hilfsfunktionen die mehrere Models/Repositories orchestrieren | `InventoryService` (Bestandsberechnung), `ProductionService` (Produktions-Workflow) |
Controller -> Repository (CRUD) -> Model
Controller -> Service (komplexe Logik) -> Repository/Model
### Weitere Backend-Entscheidungen
| Aspekt | Entscheidung |
|--------|-------------|
| Controller-Erstellung | `php artisan make:controller` mit `--no-interaction` |
| Modelle | `php artisan make:model` mit Migrations und Factories |
| Validierung | Dedizierte FormRequest-Klassen |
| Menue-Badges | Laravel **View Composer** fuer globale Badge-Zaehler |
| Einstellungen | Bestehendes `Setting`-Model mit Key-Value-Store |
### Datentypen & Praezision
| Typ | Speicherung | Begruendung |
|-----|-------------|------------|
| Gewichte | `DECIMAL(12,2)` in Gramm | Ausreichende Praezision fuer Kosmetik-Rezepturen |
| Preise | `DECIMAL(10,4)` in Euro | Netto-Preis/kg kann Nachkommastellen haben |
| Faktoren | `DECIMAL(4,2)` | Default 1.10, Bereich 1.00-9.99 |
| Mengen (Packaging) | `INT UNSIGNED` | Stueckzahlen (Tiegel, Deckel etc.) |
> **Hinweis:** Im initialen Konzept war "Milli-Cent / Milli-Gramm als Integer" vorgeschlagen. Das wird hier bewusst zu DECIMAL geaendert, weil: (a) das bestehende System durchgehend DECIMAL fuer Preise nutzt, (b) die Kosmetik-Produktion keine Nanogramm-Praezision erfordert, (c) DECIMAL(12,2) in Gramm bereits eine Genauigkeit auf 0,01g bietet.
---
## 3. Datenmodell
### Uebersicht neue Tabellen
```
STAMMDATEN
locations Lagerorte (Koeln, Waldboel ...)
supplier_categories Lieferanten-Kategorien
supplier_supplier_category Pivot: Lieferant <-> Kategorie (n:m)
suppliers Lieferanten
material_qualities Rohstoff-Qualitaeten (bio, konventionell ...)
packaging_materials Verpackungs-Wertstoffe (Glas, Pappe ...)
packaging_items Verpackungs-Artikel (Glastiegel 50ml ...)
BESTEHEND (ERWEITERT)
ingredients + default_factor, min_stock_alert
product_ingredients + pos, gram, factor
products + shelf_life_type, shelf_life_months
WARENEINGANG
stock_entries Eingangs-Datensaetze (Pending/Received)
PRODUKT-PACKAGING (BOM)
product_packagings Pivot: Produkt -> Packaging-Artikel
PRODUKTION
productions Produktionslauf (Produkt, Datum, Menge ...)
production_ingredients Zuordnung INCI -> Charge(n) pro Produktion
production_packagings Snapshot: Packaging-Verbrauch pro Produktion
AUSGANG / AUSSCHUSS
stock_disposals Entsorgungen / Ausschuss
AUDIT / LOGGING
inventory_logs Jede Bestandsveraenderung protokolliert
```
### Detaillierte Tabellenstruktur
#### `locations`
| Spalte | Typ | Beschreibung |
|--------|-----|-------------|
| id | BIGINT PK | |
| name | VARCHAR(255) | z.B. "Koeln", "Waldboel" |
| active | BOOLEAN | Default true |
| timestamps | | |
#### `supplier_categories`
| Spalte | Typ | Beschreibung |
|--------|-----|-------------|
| id | BIGINT PK | |
| name | VARCHAR(255) | z.B. "Rohstoffe", "Versandmaterial" |
| pos | TINYINT | Sortierung |
| timestamps | | |
#### `supplier_supplier_category` (Pivot)
| Spalte | Typ | Beschreibung |
|--------|-----|-------------|
| id | BIGINT PK | |
| supplier_id | BIGINT FK | -> suppliers |
| supplier_category_id | BIGINT FK | -> supplier_categories |
| timestamps | | |
> **Begruendung n:m:** Ein Lieferant kann in mehreren Kategorien taetig sein (z.B. liefert Rohstoffe *und* Verpackungen).
#### `suppliers`
| Spalte | Typ | Beschreibung |
|--------|-----|-------------|
| id | BIGINT PK | |
| name | VARCHAR(255) | Firmenname |
| url | VARCHAR(255) NULL | Webseite |
| contact_person | VARCHAR(255) NULL | Ansprechpartner |
| email | VARCHAR(255) NULL | |
| phone | VARCHAR(100) NULL | |
| country_id | UNSIGNED INT FK | -> countries (Default: DE) |
| notes | TEXT NULL | Freitextnotizen |
| active | BOOLEAN | Default true |
| timestamps | | |
| deleted_at | TIMESTAMP NULL | SoftDeletes |
#### `material_qualities`
| Spalte | Typ | Beschreibung |
|--------|-----|-------------|
| id | BIGINT PK | |
| name | VARCHAR(255) | z.B. "bio kaltgepresst", "konventionell raffiniert" |
| pos | TINYINT | Sortierung |
| timestamps | | |
#### `packaging_materials`
| Spalte | Typ | Beschreibung |
|--------|-----|-------------|
| id | BIGINT PK | |
| name | VARCHAR(255) | z.B. "Glas", "Pappe/Papier", "Kunststoff" |
| pos | TINYINT | Sortierung |
| timestamps | | |
#### `packaging_items`
| Spalte | Typ | Beschreibung |
|--------|-----|-------------|
| id | BIGINT PK | |
| packaging_material_id | BIGINT FK | -> packaging_materials |
| supplier_id | BIGINT FK NULL | -> suppliers |
| name | VARCHAR(255) | z.B. "Glastiegel 50ml", "Bambusdeckel" |
| category | ENUM | 'packaging', 'label', 'shipping_office' |
| weight_grams | DECIMAL(10,2) | Gewicht in Gramm (fuer LUCID-Abfallwirtschaft) |
| min_stock_alert | INT UNSIGNED NULL | Mindestbestand fuer Alarm (in Stueck) |
| product_id | BIGINT FK NULL | Nur fuer Etiketten: Bindung an Produkt |
| active | BOOLEAN | Default true |
| timestamps | | |
| deleted_at | TIMESTAMP NULL | SoftDeletes |
#### `ingredients` (Erweiterung)
| Spalte | Typ | Beschreibung |
|--------|-----|-------------|
| default_factor | DECIMAL(4,2) | **NEU** - Default-Schwundfaktor fuer diesen Rohstoff, Default 1.10. Wird beim Zuweisen an ein Produkt in `product_ingredients.factor` uebernommen. |
| min_stock_alert | DECIMAL(12,2) NULL | **NEU** - Globaler Meldebestand in Gramm. Bei Unterschreitung: Badge + E-Mail. |
> **Warum der Faktor am Ingredient?** Wenn z.B. Sheabutter generell einen Verschleiss von 1,2 hat, wird das einmal am Rohstoff-Stammdatum gesetzt. Bei jeder Produkt-Zuweisung wird der Wert automatisch uebernommen. Wird bei einer Produktion festgestellt, dass der Verschleiss anders ist, kann `ingredients.default_factor` nachkorrigiert werden - zukuenftige Zuweisungen erben dann den neuen Wert. Bestehende `product_ingredients.factor`-Werte bleiben unveraendert.
#### `product_ingredients` (Erweiterung)
| Spalte | Typ | Beschreibung |
|--------|-----|-------------|
| pos | SMALLINT UNSIGNED | **NEU** - Sortier-Reihenfolge |
| gram | DECIMAL(12,2) NULL | **NEU** - Grammzahl pro Einheit (Rezeptur) |
| factor | DECIMAL(4,2) | **NEU** - Individueller Schwund-Faktor pro Produkt-INCI-Kombination. Default wird aus `ingredients.default_factor` uebernommen. |
#### `products` (Erweiterung)
| Spalte | Typ | Beschreibung |
|--------|-----|-------------|
| shelf_life_type | ENUM('pao','fixed') NULL | **NEU** - PAO (12M-Tiegel) oder festes MHD |
| shelf_life_months | TINYINT UNSIGNED NULL | **NEU** - 6/12/18/24/30/36 Monate |
> **Kein globaler `waste_factor` am Produkt.** Der Faktor wird individuell pro INCI gefuehrt (`product_ingredients.factor`), mit Default aus dem Rohstoff-Stammdatum (`ingredients.default_factor`). Das ist flexibler und erfordert nur einmaliges Anlegen pro Rohstoff.
#### `product_packagings`
| Spalte | Typ | Beschreibung |
|--------|-----|-------------|
| id | BIGINT PK | |
| product_id | BIGINT FK | -> products |
| packaging_item_id | BIGINT FK | -> packaging_items |
| quantity | INT UNSIGNED | Stueck pro Produkteinheit (Default 1) |
| timestamps | | |
#### `stock_entries`
| Spalte | Typ | Beschreibung |
|--------|-----|-------------|
| id | BIGINT PK | |
| entry_type | ENUM | 'ingredient', 'packaging', 'label', 'shipping_office' |
| ingredient_id | BIGINT FK NULL | -> ingredients (wenn Rohstoff) |
| packaging_item_id | BIGINT FK NULL | -> packaging_items (wenn Packaging/Etikett/Versand) |
| supplier_id | BIGINT FK | -> suppliers |
| location_id | BIGINT FK | -> locations (Lagerort) |
| unit | ENUM('gram','piece') | Mengeneinheit: 'gram' fuer Rohstoffe, 'piece' fuer Packaging |
| **Stufe 1 - Einkauf** | | |
| ordered_by | BIGINT FK | -> users (Einkaeufer) |
| ordered_at | DATE | Kaufdatum |
| ordered_quantity | DECIMAL(12,2) | Bestellte Menge (in Gramm oder Stueck, je nach `unit`) |
| price_per_kg | DECIMAL(10,4) NULL | Netto-Preis pro kg (nur Rohstoffe) |
| price_total | DECIMAL(10,4) NULL | Gesamtpreis netto |
| **Stufe 2 - Eingang** | | |
| received_by | BIGINT FK NULL | -> users (Wareneingaenger) |
| received_at | DATE NULL | Eingangsdatum |
| received_quantity | DECIMAL(12,2) NULL | Tatsaechliche Menge |
| batch_number | VARCHAR(100) NULL | Chargen-Nr. |
| best_before | DATE NULL | MHD |
| quality_id | BIGINT FK NULL | -> material_qualities |
| **Status** | | |
| status | ENUM('pending','received') | Default 'pending' |
| timestamps | | |
> **Kein `min_stock_alert` am Wareneingang.** Der Meldebestand wird am Stammdaten-Objekt gefuehrt: `ingredients.min_stock_alert` fuer Rohstoffe, `packaging_items.min_stock_alert` fuer Packaging. Das stellt sicher, dass der Schwellenwert global und einheitlich gilt.
#### `productions`
| Spalte | Typ | Beschreibung |
|--------|-----|-------------|
| id | BIGINT PK | |
| product_id | BIGINT FK | -> products |
| location_id | BIGINT FK | -> locations (Produktionsstandort) |
| produced_by | BIGINT FK | -> users |
| produced_at | DATE | Produktionsdatum |
| quantity | INT UNSIGNED | Produzierte Stueckzahl |
| notes | TEXT NULL | |
| timestamps | | |
#### `production_ingredients`
| Spalte | Typ | Beschreibung |
|--------|-----|-------------|
| id | BIGINT PK | |
| production_id | BIGINT FK | -> productions |
| ingredient_id | BIGINT FK | -> ingredients |
| stock_entry_id | BIGINT FK | -> stock_entries (Chargen-Zuordnung) |
| quantity_used | DECIMAL(12,2) | Verbrauchte Menge in Gramm |
| timestamps | | |
> **Chargen-Splitting:** Wenn eine Charge leer wird und die naechste verwendet wird, entstehen zwei Eintraege in `production_ingredients` fuer denselben `ingredient_id` mit unterschiedlichen `stock_entry_id`.
#### `production_packagings` (Snapshot)
| Spalte | Typ | Beschreibung |
|--------|-----|-------------|
| id | BIGINT PK | |
| production_id | BIGINT FK | -> productions |
| packaging_item_id | BIGINT FK | -> packaging_items |
| quantity_used | INT UNSIGNED | Verbrauchte Stueckzahl (= productions.quantity x product_packagings.quantity zum Zeitpunkt der Produktion) |
| timestamps | | |
> **Warum ein Snapshot?** Ohne diese Tabelle muesste der Packaging-Verbrauch aus `productions.quantity x product_packagings.quantity` berechnet werden. Wird die BOM eines Produkts nachtraeglich geaendert (z.B. Tiegel-Wechsel), waeren historische Bestandsberechnungen falsch. `production_packagings` speichert den tatsaechlichen Verbrauch zum Zeitpunkt der Produktion.
#### `stock_disposals`
| Spalte | Typ | Beschreibung |
|--------|-----|-------------|
| id | BIGINT PK | |
| disposal_type | ENUM | 'ingredient', 'packaging', 'label', 'shipping_office' |
| ingredient_id | BIGINT FK NULL | -> ingredients |
| packaging_item_id | BIGINT FK NULL | -> packaging_items |
| stock_entry_id | BIGINT FK NULL | -> stock_entries (Charge, falls bekannt) |
| location_id | BIGINT FK | -> locations |
| quantity | DECIMAL(12,2) | Entsorgte Menge |
| unit | ENUM('gram','piece') | Mengeneinheit |
| reason | TEXT | Begruendung |
| disposed_by | BIGINT FK | -> users |
| disposed_at | DATE | |
| timestamps | | |
#### `inventory_logs`
| Spalte | Typ | Beschreibung |
|--------|-----|-------------|
| id | BIGINT PK | |
| loggable_type | VARCHAR(255) | Polymorphe Relation (StockEntry, Production, StockDisposal) |
| loggable_id | BIGINT | |
| action | ENUM | 'created', 'updated', 'received', 'produced', 'disposed' |
| user_id | BIGINT FK | -> users |
| changes | JSON NULL | Geaenderte Felder (alte/neue Werte) |
| timestamps | | |
---
## 4. Modul A: INCI-Management
### A1. Bugfixes (Voraussetzung)
- **`Product::product_ingredients()`** - Falschen Parameter korrigieren
- **`Ingredient::product_ingredients()`** - Falschen Parameter korrigieren
- **`ProductRepository::updateIngredients()`** - `sync()`-Logik implementieren statt nur hinzufuegen
- **`ProductRepository::copy()`** - Attribut-Kopierung fixen (`$attribute->type_id`)
### A2. Migration: Pivot erweitern
```sql
ALTER TABLE product_ingredients
ADD COLUMN pos SMALLINT UNSIGNED DEFAULT 0 AFTER ingredient_id,
ADD COLUMN gram DECIMAL(12,2) NULL AFTER pos,
ADD COLUMN factor DECIMAL(4,2) DEFAULT 1.10 AFTER gram;
```
### A3. Migration: Ingredients erweitern
```sql
ALTER TABLE ingredients
ADD COLUMN default_factor DECIMAL(4,2) DEFAULT 1.10,
ADD COLUMN min_stock_alert DECIMAL(12,2) NULL;
```
### A4. Migration: Products erweitern (Haltbarkeit)
```sql
ALTER TABLE products
ADD COLUMN shelf_life_type ENUM('pao','fixed') NULL,
ADD COLUMN shelf_life_months TINYINT UNSIGNED NULL;
```
### A5. Drag & Drop Sortierung
- **Frontend:** SortableJS auf der INCI-Tabelle im Produkt-Formular
- **Workflow:** User ordnet INCIs per Drag & Drop -> klickt "Speichern" -> AJAX-Request sendet sortierte IDs -> Backend aktualisiert `pos`-Spalte
- Kein Live-Speichern pro Verschiebung
### A6. Alphabetisches Dropdown + Klick-Reihenfolge
- `HTMLHelper::getProductIngredientsOptions()` erhaelt `orderBy('name')`
- Select2-Widget mit Mehrfachauswahl
- Die Reihenfolge der Auswahl bestimmt die initiale `pos` beim Hinzufuegen
### A7. Gramm & Faktor-Felder
- In der INCI-Tabelle erscheinen pro Zeile zusaetzlich:
- Eingabefeld "Gramm" (DECIMAL)
- Eingabefeld "Faktor" (DECIMAL, Default aus `ingredients.default_factor`)
- Berechnete Spalte "Effektiv" zeigt `gram x factor`
- Beim Hinzufuegen eines INCI zum Produkt wird `factor` automatisch aus `ingredients.default_factor` uebernommen
### A8. Haltbarkeit (MHD)
- Neue Felder im Produkt-Formular (Abschnitt "Warenwirtschaft"):
- Radio-Button: "12M-Tiegel (PAO)" / "Festes MHD"
- Dropdown bei "Festes MHD": 6, 12, 18, 24, 30, 36 Monate
- Gespeichert in `products.shelf_life_type` + `products.shelf_life_months`
### A9. Kopierfunktion erweitern
- `ProductRepository::copy()` muss die neuen Pivot-Felder `pos`, `gram`, `factor` mitkopieren
- Deterministisches `orderBy('pos')` beim Lesen der INCIs
---
## 5. Modul B: Stammdaten
### B1. Lagerorte (`locations`)
Einfache CRUD-Seite. Felder: Name, Aktiv-Status.
### B2. Lieferanten-Kategorien (`supplier_categories`)
Einfache CRUD-Seite. Felder: Name, Sortierung.
### B3. Lieferanten (`suppliers`)
- Listenansicht mit DataTable + Suchfeld
- Formular: Kategorien (**Mehrfachauswahl** via Select2 -> Pivot `supplier_supplier_category`), Name, URL, Ansprechpartner, E-Mail, Telefon, Land (Dropdown, Default DE), Notizen
- SoftDeletes fuer Datenintegritaet
### B4. Rohstoff-Qualitaeten (`material_qualities`)
Einfache CRUD-Seite. Vorbelegte Eintraege:
- konventionell
- bio kaltgepresst
- bio raffiniert
- konventionell kaltgepresst
- konventionell raffiniert
### B5. Verpackungs-Materialien (`packaging_materials`)
Einfache CRUD-Seite. Vorbelegte Eintraege:
- Glas
- Holz/Bambus
- Pappe/Papier
- Kunststoff
### B6. Verpackungs-Artikel (`packaging_items`)
- Formular: Name, Material (Dropdown -> packaging_materials), Kategorie (Packaging/Etikett/Versand & Office), Gewicht (Gramm), Lieferant (Dropdown), Mindestbestand
- Etiketten: Zusaetzliches Feld fuer Produkt-Zuordnung
- Listenansicht mit Filter nach Kategorie
---
## 6. Modul C: Wareneingang
### Zwei-Stufen-Workflow
**Stufe 1 - Einkaeufer erstellt Bestellung:**
- Dropdown: Kategorie (Rohstoff / Packaging / Etikett / Versand & Office)
- `unit` wird automatisch gesetzt: 'gram' bei Rohstoff, 'piece' bei Packaging/Etikett/Versand
- Je nach Kategorie:
- **Rohstoff:** Lieferant, Inhaltsstoff (Select2 Autosuggest), Menge (g), Netto-Preis/kg, Lagerort
- **Packaging etc.:** Lieferant, Artikel (Select2), Menge (Stueck), Preis, Lagerort
- Status: **Pending** (oranger Punkt)
**Stufe 2 - Wareneingang buchen:**
- Ivonne oeffnet den Pending-Eintrag und ergaenzt:
- Eingangsdatum, tatsaechliche Menge, Chargen-Nr., MHD, Qualitaet/Sorte (nur Rohstoffe)
- Status wechselt zu: **Received** (gruener Punkt)
### Sortierung der Liste
1. Pending-Eintraege oben, sortiert nach Bestelldatum (neueste oben)
2. Eingetroffene Ware darunter, sortiert nach Eingangsdatum
### Alarm-Logik
- Meldebestand am Stammdatum: `ingredients.min_stock_alert` fuer Rohstoffe, `packaging_items.min_stock_alert` fuer Packaging
- Bestand-Check: `SUM(received) - SUM(produced) - SUM(disposed)`
- Bei Unterschreitung: roter Badge im Menue + E-Mail an konfigurierbare Adresse (Setting)
---
## 7. Modul D: Rezeptur & BOM
### Rezeptur (Rohstoffe)
Bereits in Modul A abgedeckt: INCI-Pivot mit `gram` und `factor`.
**Verbrauchsberechnung bei Produktion:**
```
Verbrauch pro Einheit = gram x factor
Verbrauch gesamt = gram x factor x produzierte_stueckzahl
```
### BOM - Bill of Materials (Packaging)
Am Produkt werden ueber `product_packagings` die Verpackungskomponenten zugewiesen:
| Produkt: Pechsalbe | Packaging-Artikel | Menge |
|---------------------|-------------------|-------|
| | Glastiegel 50ml | 1 |
| | Bambusdeckel | 1 |
| | Umverpackung Pechsalbe | 1 |
| | Flyer Pechsalbe | 1 |
**UI im Produkt-Formular:**
- Neuer Abschnitt "Verpackung & Material"
- Select2-Dropdown zur Auswahl von Packaging-Artikeln
- Pro Zeile: Artikelname, Material (readonly aus Stammdaten), Gewicht (readonly), Menge pro Einheit
- Sortierbar
### LUCID-Report (Abfallwirtschaft)
Aus den Produktionsdaten laesst sich berechnen:
```
Produzierte Menge x Packaging-Gewicht -> kg pro Material
```
Beispiel: 500 Pechsalben x 120g Glas = 60 kg Glas in Verkehr gebracht.
> Report-Funktion wird als spaetere Erweiterung empfohlen (nicht im MVP).
---
## 8. Modul E: Produktion
### Workflow
1. User klickt "Neue Produktion"
2. Dropdown: Produkt auswaehlen
3. System laedt automatisch:
- Alle INCIs des Produkts (mit Grammzahl und Faktor)
- Alle Packaging-Artikel des Produkts
4. User fuellt aus:
- Datum (Default: heute)
- Stueckzahl
- Produktionsstandort (Dropdown -> locations)
5. Pro INCI: Auswahl der Charge(n) aus Dropdown der letzten 3 empfangenen `stock_entries` dieses Inhaltsstoffs am gewaehlten Standort
- Mehrfachauswahl moeglich (Chargen-Splitting)
- Bei Splitting: User gibt pro Charge die verwendete Menge ein
### Bestandsabzug
Bei Speicherung:
1. **Rohstoffe:** `production_ingredients`-Eintraege werden erstellt. Bestand wird rechnerisch reduziert.
2. **Packaging:** `production_packagings`-Eintraege werden als Snapshot erstellt (= `productions.quantity x product_packagings.quantity`). Bestand wird rechnerisch reduziert.
> **Kein physisches Bestandsfeld:** Der aktuelle Bestand wird immer berechnet:
> `Bestand = SUM(Eingaenge received) - SUM(Produktion consumed) - SUM(Ausgang disposed)`
> Das vermeidet Inkonsistenzen durch doppelte Datenhaltung.
> **Warum Snapshots fuer Packaging?** Wird die BOM eines Produkts nachtraeglich geaendert (z.B. Tiegel-Wechsel), bleiben historische Produktionen korrekt, weil der tatsaechliche Verbrauch zum Produktionszeitpunkt in `production_packagings` festgehalten ist.
### MHD-Warnung
Wenn bei der Chargen-Auswahl ein Rohstoff-MHD (aus `stock_entries.best_before`) kuerzer ist als das geplante Produkt-MHD, erscheint eine gelbe Warnung.
---
## 9. Modul F: Bestandsuebersicht & Alarme
### Getrennte Views
Vier separate Bestandsseiten (je ein Menuepunkt):
1. **Bestand Rohstoffe**
2. **Bestand Packaging**
3. **Bestand Etiketten**
4. **Bestand Versand & Office**
### Darstellung pro Seite
Die Lager-Spalten werden **dynamisch** aus der `locations`-Tabelle generiert. Kein Hardcoding von Standortnamen.
| Spalte | Beschreibung |
|--------|-------------|
| Artikel | Name des Rohstoffs / Packaging-Artikels |
| *Lager [Name]* | Bestand am Standort (dynamisch pro Location) |
| **Gesamt** | Summe aller Standorte |
| Meldebestand | Schwellenwert |
| Status | OK / Unter Meldebestand |
- DataTable mit Sortierung und Suche
- Zeilen unter Meldebestand werden rot hervorgehoben
- Wenn neue Lagerorte angelegt werden, erscheinen automatisch neue Spalten
### Menue-Badges (View Composer)
Ein `InventoryBadgeComposer` wird als Laravel View Composer registriert und an das Sidenav-Layout gebunden:
```php
// AppServiceProvider::boot()
View::composer('layouts.includes.layout-sidenav', InventoryBadgeComposer::class);
```
Der Composer zaehlt Artikel unter Meldebestand pro Kategorie und gibt die Zahlen an die View weiter. Im Menue werden rote Badges angezeigt (z.B. "Bestand Rohstoffe (2)").
**Performance:** Query-Ergebnis wird per Cache (5 Minuten) zwischengespeichert.
### E-Mail-Alarm
- Cron-Job (taeglich): Prueft alle Bestaende gegen Meldebestand
- Bei Unterschreitung: E-Mail an konfigurierbare Adresse (Setting: `inventory_alert_email`, Default: `service@gruene-seele.bio`)
- Registrierung in `app/Console/Kernel.php` (konsistent mit bestehenden Cron-Jobs)
---
## 10. Modul G: Ausgang / Ausschuss
### Felder
- Typ (Rohstoff / Packaging / Etikett / Versand & Office)
- Artikel (Select2, abhaengig vom Typ)
- Charge (Dropdown, falls bekannt)
- Lagerort (Dropdown)
- Menge
- Einheit (automatisch: gram/piece je nach Typ)
- Grund (Pflicht-Textfeld)
- Datum (Default: heute)
- User: Automatisch aus Session
### Listenansicht
DataTable mit allen Entsorgungen, filterbar nach Typ und Zeitraum.
---
## 11. Modul H: Rollen, Berechtigungen & Audit-Trail
### Berechtigungs-Konzept
Das bestehende Rollen-System ist hierarchisch und ausreichend:
| Rolle | Level | Warenwirtschafts-Rechte |
|-------|-------|------------------------|
| CopyReader (Redakteur/Ivonne) | admin >= 1 | Wareneingang buchen, Produktion eintragen, Ausgang erfassen, Bestaende einsehen |
| Admin | admin >= 7 | Wie CopyReader + Lieferanten verwalten, Einkauf anlegen, Preise sehen |
| SuperAdmin | admin >= 8 | Wie Admin + Stammdaten verwalten (Lagerorte, Qualitaeten, Materialien), Einstellungen |
**Umsetzung:** Sensible Felder (Netto-Preise) und Stammdaten-Verwaltung werden ueber bestehende Middleware geschuetzt. Fuer die Stufe-2-Felder (Wareneingang buchen) reicht `copyreader`.
### Audit-Trail
Jede Bestandsveraenderung wird in `inventory_logs` protokolliert:
- **Polymorphe Relation:** Verknuepft mit `StockEntry`, `Production`, oder `StockDisposal`
- **User-ID:** Automatisch aus `Auth::id()`
- **Changes:** JSON mit alten und neuen Werten
- Implementierung ueber Laravel Model Events (Observer-Pattern)
---
## 12. Modul I: Warenwirtschafts-Einstellungen
### Konzept
Konfigurierbare Einstellungen fuer die Warenwirtschaft werden ueber das bestehende `Setting`-Model (Key-Value-Store) verwaltet. Die Oberflaeche wird analog zum bestehenden `SettingController` (`app/Http/Controllers/SettingController.php`) aufgebaut.
### Settings
| Slug | Typ | Default | Beschreibung |
|------|-----|---------|-------------|
| `inventory_alert_email` | text | `service@gruene-seele.bio` | E-Mail-Adresse fuer Bestands-Alarme |
| `inventory_alert_enabled` | bool | true | Bestands-Alarm aktiviert? |
| `inventory_default_location` | int | (erste Location) | Standard-Lagerort fuer neue Eingaenge |
### Zugriff
- View: `resources/views/admin/inventory/settings.blade.php`
- Controller: `Admin/Inventory/InventorySettingController`
- Middleware: `superadmin`
- Menuepunkt unter "Warenwirtschaft" ganz unten
### Erweiterbarkeit
Weitere Einstellungen koennen jederzeit ueber `Setting::setContentBySlug()` ergaenzt werden, ohne Migrationen oder Code-Aenderungen am Settings-Mechanismus.
---
## 13. Menuestruktur
Neuer Block im Sidenav zwischen "Produkte" (CopyReader) und dem Admin-Bereich:
```
Warenwirtschaft
Eingang [copyreader] Badge: Pending-Anzahl
Ausgang [copyreader]
Produktion [copyreader]
----
Bestand Rohstoffe [copyreader] Badge: Unter Meldebestand
Bestand Packaging [copyreader] Badge: Unter Meldebestand
Bestand Etiketten [copyreader] Badge: Unter Meldebestand
Bestand Versand & Office [copyreader] Badge: Unter Meldebestand
----
Lieferanten [admin]
Lieferanten Kategorien [admin]
----
Lagerorte [superadmin]
Qualitaeten / Sorten [superadmin]
Verpackungs-Materialien [superadmin]
Einstellungen [superadmin]
```
---
## 14. Abweichungen vom initialen Konzept
| Thema | Initiales Konzept | Finale Entscheidung | Begruendung |
|-------|-------------------|---------------------|------------|
| Frontend-Stack | Livewire empfohlen | jQuery + Select2 + SortableJS | Konsistenz mit bestehendem Stack, kein Architekturbruch |
| Datentypen | Milli-Cent / Milli-Gramm (Integer) | DECIMAL | Konsistenz mit bestehendem Schema, ausreichende Praezision |
| Bestandsfuehrung | Nicht spezifiziert | Berechneter Bestand (kein physisches Feld) | Vermeidet Inkonsistenzen |
| Etiketten | Eigenstaendige Tabelle | Ueber `packaging_items` mit `category='label'` + optionaler `product_id` | Weniger Tabellen, gleiche Funktionalitaet |
| Meldebestand | Am Wareneingang | Am Stammdaten-Objekt (`ingredients` / `packaging_items`) | Global gueltig, nicht pro Eingang |
| Waste-Faktor | Globaler Faktor am Produkt + pro INCI | Nur pro INCI (`product_ingredients.factor`), Default aus `ingredients.default_factor` | Flexibler, einmaliges Anlegen pro Rohstoff, nachkorrigierbar |
| Lieferanten-Kategorien | 1:1 (FK) | n:m (Pivot `supplier_supplier_category`) | Ein Lieferant kann mehrere Kategorien bedienen |
| Packaging-Bestandsabzug | Implizit berechnet aus BOM | Snapshot in `production_packagings` | BOM-Aenderungen verfaelschen sonst historische Bestaende |
| Mengeneinheiten | Implizit aus entry_type | Explizite `unit`-Spalte (ENUM: 'gram', 'piece') | Eindeutig, keine fragile Ableitung noetig |
| Lager-Spalten | Hardcoded | Dynamisch aus `locations`-Tabelle | Erweiterbar auf n Standorte ohne Code-Aenderung |
| Einstellungen | Nicht vorgesehen | Settings via bestehendem `Setting`-Model | Nichts hardcoden, erweiterbar |
| LUCID-Report | Als Feature | Als spaetere Erweiterung | MVP-Fokus |
| Neue Redakteur-Rolle | Neue Rolle "Redakteur" | Bestehende CopyReader-Rolle (admin >= 1) | Existiert bereits, deckt den Use-Case ab |
| Architektur-Pattern | Nicht spezifiziert | Repositories fuer CRUD, Services fuer uebergreifende Logik | Konsistent mit bestehendem Code |

View file

@ -0,0 +1,239 @@
## Feedback zum Briefing
### A) INCI-Handling
* **Kopierfunktion:** Das ist ein klassischer Bug im `replicate()`-Prozess von Laravel. Wenn die INCIs über eine Pivot-Tabelle verknüpft sind, müssen die Relationen explizit mitkopiert werden.
* **Sortierung:** Die Idee, erst zu sortieren und dann "global" zu speichern, schont die Datenbank. Ich empfehle hier **SortableJS**. Es ist performant und erlaubt flüssiges Drag & Drop.
* **Dropdown-Logik:** Technisch muss hier sichergestellt werden, dass die Datenbank die `order`-Spalte nicht nach Alphabet, sondern nach der `timestamp` oder `index` des Klicks speichert. Das ist ein wichtiger Hinweis für die Backend-Logik!
### B) & D) Warenwirtschaft & Eingang
* **Datenmodell:** Wir brauchen eine klare Trennung zwischen **"Bestellung/Pending"** und **"Eingang/Gebucht"**.
* **Autosuggest:** Bei über 200 Rohstoffen ist eine Bibliothek wie *Select2* oder *TomSelect* sinnvoll, damit Ivonne nicht scrollen muss, sondern tippen kann.
* **Alarm-Funktion:** Das Feld für die Mindestmenge (z.B. 5.000g) sollte direkt am **Stammdaten-Objekt des Rohstoffs** hängen, nicht nur im Wareneingang, damit der Alarm global funktioniert.
### C) Rezepturen & Faktoren
* **Der "Verschleiß-Faktor":** Das ist ein extrem wichtiger Punkt für die Kalkulation.
* *Berechnungslogik:* Wenn die Rezeptur 100g vorsieht und der Faktor 1,1 ist, zieht das System bei der Produktion automatisch 110g aus dem Bestand ab. Das ist sauber gelöst.
* **Dichte-Thematik (Öl/Wasser):** Da du sagst, ihr rechnet konsequent in **Gramm**, ist die Dichte für die reine Bestandsführung im System egal solange ihr beim Wiegen an der Waage bleibt. Das System rechnet einfach: `Eingang (g) - Verbrauch (g) * Faktor`.
### E) Produktion & Chargen-Tracking
* **FIFO-Prinzip:** Da Ivonne die letzten 3 Chargen sieht, ist das System perfekt für die Rückverfolgbarkeit (wichtig für Kosmetik-Verordnungen!).
* **Mehrfachauswahl bei Chargen:** Das ist klug, falls eine Charge während der Produktion leer geht und die nächste angebrochen wird.
### F) Bestand & UI
* **Menü-Badges:** Die roten Zahlen im Menü (z.B. "Bestand Rohstoffe (2)") sind ein hervorragender Trigger. Technisch lösen wir das über einen `View Composer` in Laravel, damit die Zahl auf jeder Seite aktuell bleibt.
### G) Ausgang (Ausschuss)
* **Logging:** Da du "Wer" (User-ID) und "Warum" abfragst, ist das ein vollwertiges Korrekturjournal. Das ist für Audits Gold wert.
---
----------
### 1. Die "Stückliste" (BOM - Bill of Materials)
Du erweiterst das System von einer reinen Rezeptur (INCIs) zu einer vollständigen **Stückliste**. Das bedeutet, ein "Produkt" besteht in Laravel nun aus zwei Relationen:
* **Rezeptur:** (Rohstoffe in Gramm * Faktor)
* **Packaging:** (Tiegel, Deckel, Etikett, Umkarton in Stück)
**Wichtig für die Logik:** Im Gegensatz zu den Rohstoffen brauchen Verpackungen keinen "Schwund-Faktor" von 1,1, sondern werden meist als **Ganzzahlen** (Stück) abgezogen. Es sei denn, du hast auch hier nennenswerten Ausschuss (z.B. Etiketten, die beim Bekleben kaputtgehen) dann sollte der Faktor auch hier optional zuschaltbar sein.
### 2. Der Workflow: Erst Definition, dann Zuordnung
Dein Ansatz ist vollkommen richtig:
1. **Stammdaten:** Erst legst du unter "Warenwirtschaft > Bestand Verpackungen" den Artikel an (z.B. "Glastiegel 40ml").
2. **Produkt-Konfiguration:** Beim Produkt (z.B. Tattoocreme) sagst du nun: "Dieses Produkt verbraucht pro Einheit 1x Glastiegel 40ml".
3. **Produktions-Trigger:** Sobald Ivonne 500 Einheiten produziert, zieht das System im Hintergrund automatisch 500 Tiegel vom Bestand ab.
### 3. Visualisierung der Datenstruktur
Damit die Datenbank sauber bleibt, empfehle ich folgende Struktur für die Produktanlage:
| Ebene | Typ | Einheit | Besonderheit |
| --- | --- | --- | --- |
| **A (Rohstoffe)** | INCI / Rohstoff | Gramm | Inkl. Faktor (1,1) & Chargen-Tracking |
| **B (Primärpackung)** | Tiegel / Flasche | Stück | Zwingend für Produktion nötig |
| **C (Sekundärpackung)** | Karton / Beipackzettel | Stück | Optional, falls im System geführt |
### 4. Usability-Tipp für die Verpackungsliste
Da die Verpackungen oft für viele Produkte gleich sind (der gleiche Tiegel für 5 verschiedene Cremes), sollte die Auswahl im Backend über eine **Suche/Dropdown-Kombination** erfolgen, genau wie bei den INCIs.
### 5. Der "Bestand-Alarm" für Verpackungen
Da Verpackungen oft lange Lieferzeiten haben, solltest du hier die gleiche **Alarm-Logik** (Mail an Service) wie bei den Rohstoffen implementieren, sobald der "Meldebestand" unterschritten wird.
---
---
# neues erweitertes Briefing: Erweiterung Web-CRM „Warenwirtschaft & Produktion“
## 1. Überarbeitung INCI-Management (Produkte)
Ziel ist eine intuitivere Verwaltung der Inhaltsstoffe.
* **Kopierfunktion:** Beim Duplizieren eines Produkts müssen alle verknüpften INCIs (Pivot-Daten) mitkopiert werden.
* **Drag & Drop Sortierung:** INCIs innerhalb eines Produkts müssen manuell sortierbar sein (z. B. via SortableJS). Speicherung der Reihenfolge erfolgt erst beim Klick auf „Speichern“ (kein Live-DB-Ping pro Verschiebung).
* **Selektions-Logik:** Die Mehrfachauswahl im Dropdown muss alphabetisch sortiert sein, die Übernahme in die Produktliste erfolgt jedoch in der **Reihenfolge der Auswahl (Klicks)**.
## 2. Stammdaten & Menüstruktur „Warenwirtschaft“
Ein neuer Hauptmenüpunkt „Warenwirtschaft“ mit folgenden Unterpunkten wird erstellt:
1. **Eingang:** Verwaltung von Bestellungen und Lieferungen (Status: Pending/Grün).
2. **Ausgang:** Erfassung von Ausschuss (Schrottliste) mit Grund, User-Log und Datum.
3. **Produktion:** Protokollierung der Herstellungsprozesse.
4. **Lieferanten:** Verwaltung inkl. Kategorien (Rohstoffe, Tiegel, Verpackung etc.).
5. **Bestand (Getrennte Views):**
* Bestand Rohstoffe
* Bestand Packaging (Hardware: Tiegel, Deckel)
* Bestand Etiketten (Produktbezogen)
* Bestand Versand & Office
## 3. Rezeptur & Kalkulations-Logik (Backend-Specs)
Um Rundungsfehler zu vermeiden, wird intern mit erhöhter Präzision gerechnet:
* **Datentypen:** Preise in **Milli-Cent** (Integer), Gewichte in **Milli-Gramm** (Integer oder Decimal 12,4).
* **Verbrauchs-Faktor:** Jedes Produkt erhält pro INCI ein Feld für die Grammzahl und einen **Schwund-Faktor** (Default 1,1).
* *Berechnung:* `Soll-Menge (g) * Faktor = Tatsächlicher Bestandsabzug`.
* **Haltbarkeit:** Auswahl pro Produkt via Radio-Button:
* a) Symbol „12M-Tiegel“ (PAO - Period After Opening).
* b) Festes MHD (Auswahl: 6, 12, 18, 24, 30, 36 Monate).
## 4. Workflow: Wareneingang (Zwei-Stufen-System)
1. **Stufe 1 (Einkauf):** Erfassung von Lieferant, Inhaltsstoff (Autosuggest), Kaufdatum, Menge (g) und Netto-Preis/kg. Status steht auf **„Pending“ (Orange)**.
2. **Stufe 2 (Ivonne/Eingang):** Bei Erhalt der Ware Ergänzung von Eingangsdatum, tatsächlicher Menge (g), **Chargen-Nummer** und **MHD**. Status wechselt auf **„Eingebucht“ (Grün)**.
3. **Bestands-Alarm:** Pro Artikel (Rohstoff/Verpackung) wird ein Schwellenwert definiert. Bei Unterschreitung erscheint ein **roter Badge (Zahl)** am Menüpunkt und eine E-Mail geht an den Service.
## 5. Workflow: Produktion & Bestandsabzug
Bei einer neuen Produktion (z.B. 500 Einheiten Tattoocreme):
* **Rohstoff-Abzug:** Das System listet alle hinterlegten INCIs auf. Ivonne wählt pro INCI aus den **letzten 3 Chargen** (FIFO-Prinzip) die verwendeten aus.
* **Chargen-Splitting:** Es muss möglich sein, zwei Chargen für einen Rohstoff zu wählen, falls eine leer wird.
* **Packaging-Abzug:** Automatische Reduktion der verknüpften Hardware (Tiegel/Deckel) und des spezifischen Etiketts um die produzierte Stückzahl.
* **MHD-Check:** Warnmeldung, falls ein Rohstoff-MHD kürzer ist als das geplante Produkt-MHD.
## 6. Etiketten-Sonderlogik
Etiketten werden wie Packaging behandelt, aber:
* Sie sind fest an eine **Produkt-ID** gebunden.
* Wareneingang erfolgt meist manuell nach einem Druckauftrag (z.B. 500 Stück).
* Der Bestand zeigt den Restbestand nach Produktion an (wichtig für Nachbestellungen).
## 7. Rollen & Sicherheit
* **Berechtigung:** Einführung einer Rolle „Redakteur“ (Ivonne). Diese darf Bestände buchen und Produktionen eintragen, aber keine System-Stammdaten oder sensible Einkaufspreise löschen/ändern (falls konfiguriert).
* **Logging:** Jede Bestandsveränderung (Eingang, Produktion, Ausschuss) wird mit der `user_id` protokolliert.
---
Das sind hervorragende und extrem praxisnahe Ergänzungen! Besonders die **LUCID-Verpackungsregister-Thematik** (Abfallwirtschaft) und die **Multi-Lager-Logik** (Köln & Waldbröl) heben das System von einem einfachen CRM zu einem echten, professionellen ERP-System (Enterprise Resource Planning) auf.
Diese Punkte greifen tief in die Datenbankstruktur ein, lassen sich mit Laravel aber sehr sauber abbilden.
Hier ist das **aktualisierte, vollständige Briefing (Version 2.0)**, das du 1:1 an deinen Entwickler weitergeben kannst. Die neuen Punkte sind nahtlos in die Architektur integriert.
---
# Entwickler-Briefing V2.0: Web-CRM „Warenwirtschaft, Produktion & Bestände“
*(Empfohlener Tech-Stack für interaktive Module: Laravel + Livewire + Bootstrap)*
## 1. Überarbeitung INCI-Management (Produkte)
* **Kopierfunktion:** Beim Duplizieren (`replicate()`) eines Produkts müssen alle verknüpften INCIs (Pivot-Daten) zwingend mitkopiert werden.
* **Drag & Drop Sortierung:** INCIs innerhalb eines Produkts müssen manuell sortierbar sein (z. B. via *SortableJS* + Livewire). Speicherung der Reihenfolge erfolgt erst beim Klick auf „Speichern“.
* **Selektions-Logik:** Die Mehrfachauswahl im Dropdown ist alphabetisch sortiert. Die Übernahme in die Produktliste erfolgt jedoch exakt in der **Reihenfolge der Klicks**.
## 2. Stammdaten & Menüstruktur „Warenwirtschaft“
Das System wird um neue Stammdaten-Tabellen erweitert, die vorab verwaltbar sein müssen. Neues Menü:
1. **Stammdaten (Neu):**
* **Lagerorte:** Anlage von Standorten (z.B. Köln, Waldbröl).
* **Qualitäten/Sorten:** Anlage von Rohstoff-Klassifizierungen (z.B. *konventionell, bio kaltgepresst, bio raffiniert, konventionell kaltgepresst, konventionell raffiniert*).
* **Verpackungs-Materialien:** Anlage von Wertstoffen für die Abfallwirtschaft (z.B. *Glas, Holz/Bambus, Pappe/Papier, Kunststoff*).
* **Lieferanten Kategorien:** (z.B. *Rohstoffe, Tiegel, Verpackung*).
* **Lieferanten:** (inkl. Zuweisung einer Kategorie).
2. **Eingang:** Verwaltung von Bestellungen und Lieferungen.
3. **Ausgang:** Erfassung von Ausschuss (Schrottliste) mit Grund, User-Log und Datum.
4. **Produktion:** Protokollierung der Herstellungsprozesse.
5. **Bestand (Getrennte Views & Multi-Lager fähig):**
* Bestand Rohstoffe
* Bestand Packaging (Hardware: Tiegel, Deckel)
* Bestand Etiketten
* Bestand Versand & Office
## 3. Architektur: Datenmodell & Kalkulations-Logik
Um Rundungsfehler und ungenaue Meldungen (LUCID/Verpackungsregister) zu vermeiden:
* **Präzision:** Preise in **Milli-Cent** (Integer), Gewichte in **Milli-Gramm** (Integer oder Decimal 12,4).
* **Rohstoff-Verbrauch (Rezeptur):** Jedes Produkt hat Pivot-Felder für Grammzahl und einen **Schwund-Faktor** (Default 1,1). Berechnung: `Soll-Menge (g) * Faktor`.
* **MHD-Logik (Produkt):** Auswahl via Radio-Button: a) „12M-Tiegel“ (PAO) oder b) Festes MHD (6, 12, 18, 24, 30, 36 Monate).
### NEU: Packaging & Abfallwirtschaft (BOM - Bill of Materials)
Wenn ein Produkt angelegt wird und Packaging zugewiesen bekommt (z.B. Glastiegel, Bambusdeckel, Umverpackung), zieht das System seine Daten aus der Packaging-Datenbank.
* **Stammdaten Packaging:** Jeder Artikel (z.B. "Glastiegel 50ml") bekommt zwei Pflichtfelder:
1. **Material:** (Dropdown aus den zuvor angelegten Verpackungs-Materialien, z.B. *Glas*).
2. **Gewicht:** (Eingabefeld in Gramm).
* **Nutzen:** Das System kann so auf Knopfdruck einen Report generieren: *"Für 500 produzierte Pechsalben wurden X kg Glas und Y kg Pappe in Verkehr gebracht."*
## 4. Workflow: Wareneingang & Multi-Lager
1. **Stufe 1 (Einkauf / Pending):** * Erfassung von: Lieferant, Inhaltsstoff (Autosuggest), Kaufdatum, Menge (g), Netto-Preis/kg, **Lagerort (Dropdown: Köln/Waldbröl)**. Status steht auf **„Pending“ (Orange)**.
2. **Stufe 2 (Eingangsbuchung / Grün):** * Bei Erhalt ergänzt der User (z.B. Ivonne): Eingangsdatum, tatsächliche Menge (g), Chargen-Nummer, MHD und **Qualität/Sorte** (Dropdown: *bio kaltgepresst, konventionell etc.*). Status wechselt auf **„Eingebucht“ (Grün)**.
3. **Bestands-Ansicht & Alarme:** * Bestände müssen nun **pro Lagerort** sowie als **Gesamtsumme** darstellbar sein.
* Unterschreitet der Gesamtbestand den Schwellenwert, erscheint ein roter Badge am Menüpunkt + E-Mail-Alarm.
## 5. Workflow: Produktion & Bestandsabzug
Bei einer neuen Produktion (z.B. Tattoocreme):
* **Rohstoff-Abzug & Chargen-Splitting:** Das System listet die INCIs. Der User wählt aus den **letzten 3 Chargen**. Es muss möglich sein, Mengen auf zwei Chargen aufzuteilen, wenn eine Charge leer wird.
* **Lager-Logik:** Bei der Produktion muss angegeben werden, **an welchem Standort** (Köln oder Waldbröl) produziert wird, damit das System die Rohstoffe vom richtigen Lager abzieht.
* **Packaging-Abzug:** Automatische Reduktion der verknüpften Hardware (Tiegel/Deckel) und Etiketten am jeweiligen Standort.
* **MHD-Check:** Warnmeldung, falls ein Rohstoff-MHD kürzer ist als das geplante Produkt-MHD.
## 6. Rollen & Sicherheit (Activity Logging)
* **Berechtigung („Redakteur“):** Darf Bestände buchen und Produktionen eintragen, aber keine System-Stammdaten (Materialien, Qualitäten, Lagerorte) oder Preise bearbeiten/löschen.
* **Audit Trail:** Jede Bestandsveränderung (Eingang, Produktion, Ausgang/Schrott) wird zwingend mit `user_id`, `timestamp` und Begründung protokolliert.
---