gruene-seele/dev/product management /konzept-final.md

710 lines
29 KiB
Markdown

# 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 |