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

29 KiB

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
  2. Architektur-Entscheidungen
  3. Datenmodell
  4. Modul A: INCI-Management (Bugfix & Erweiterung)
  5. Modul B: Stammdaten
  6. Modul C: Wareneingang (Zwei-Stufen-System)
  7. Modul D: Rezeptur & BOM (Bill of Materials)
  8. Modul E: Produktion & Bestandsabzug
  9. Modul F: Bestandsuebersicht & Alarme
  10. Modul G: Ausgang / Ausschuss
  11. Modul H: Rollen, Berechtigungen & Audit-Trail
  12. Modul I: Warenwirtschafts-Einstellungen
  13. Menuestruktur
  14. Abweichungen vom initialen Konzept

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Ⓜ️ 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

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

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)

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:

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