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