878 lines
43 KiB
Markdown
878 lines
43 KiB
Markdown
# Entwicklungsplan: Warenwirtschaft & Produktion
|
||
|
||
> **Version:** 2.0 - Stand 10.04.2026
|
||
> **Referenz:** `konzept-final.md` (V1.1), `feedback.md` (Kundin, April 2026)
|
||
> **Geschaetzte Gesamtdauer:** 12-15 Wochen (1 Entwickler)
|
||
|
||
---
|
||
|
||
## Umsetzungsprotokoll (Ist-Stand)
|
||
|
||
| Datum | Phase | Kurzbeschreibung |
|
||
|-------|-------|------------------|
|
||
| 27.03.2026 | **0** | Bugfixes Produkt/INCI, Sync-Logik, HTMLHelper-Sortierung, Kopie `type_id`; Pest-Tests `tests/Feature/ProductPhase0Test.php`; zwei Migrationskorrekturen (siehe Phase 0). |
|
||
| 27.03.2026 | **1** | Migrationen Rezeptur/Haltbarkeit/Stammdaten INCI; UI Produktformular SortableJS (CDN) + Select2; Pivot `pos`/`gram`/`factor`; Warenwirtschaft Haltbarkeit; `product_inci_sync_sent`; Ingredient-Formular `default_factor`/`min_stock_alert`; Tests `ProductPhase1Test.php`. |
|
||
| 27.03.2026 | **2** | Stammdaten-CRUD unter `admin/inventory`: Models/Relations, Repositories `SupplierRepository`/`PackagingItemRepository`, Resource-Controller + `App\Http\Requests\Inventory\*`, Views DataTables + Select2, Sidenav; Migrationen Pivot-Unique kurz (`supplier_supplier_cat_unique`), `packaging_items.product_id` als `unsignedInteger` (Kompatibilitaet `products.id`); Seeder `InventoryStammdatenSeeder`; Tests `InventoryPhase2Test.php`; `layouts.layout-2` ergaenzt `@yield('scripts')`. |
|
||
| 27.03.2026 | **2** | Erweiterung `InventoryPhase2Test.php`: vollstaendiges CRUD je Stammdaten-Entitaet (inkl. Soft-Delete Lieferant/Verpackungsartikel, n:m Lieferant-Kategorien). |
|
||
| 27.03.2026 | **3** | Wareneingang: `stock_entries`, `StockEntry`, `StockEntryRepository`, `StockEntryController`, FormRequests, Routen + API-Suche (Select2), Views index/create/edit/show + `_scripts` (Select2/toggle), Sidenav-Link fuer CopyReader; Tests `InventoryPhase3Test.php`. |
|
||
| 27.03.2026 | **4** | Pivot `product_packagings` (quantity, pos), `Product::packagings()` / `PackagingItem::products()`, `ProductRepository::updatePackagings` + Kopie; Produktformular Abschnitt „Verpackung & Material“ mit Modal, SortableJS; Tests `ProductPhase4Test.php`. |
|
||
| 27.03.2026 | **5** | Produktion: Tabellen `productions`, `production_ingredients`, `production_packagings`; `ProductionService` (Transaktion, Soll/Ist Gramm, MHD-Warnung, Packaging-Snapshot); `ProductionController` + Rezept-API; Views index/create/show; Sidenav; Tests `ProductionPhase5Test.php`. |
|
||
| | 6–9 | *noch offen* |
|
||
|
||
| 10.04.2026 | **5.1a** | Feedback Menue & Stammdaten: Sidenav-Umbenennungen (Rohstoffqualitaet, Verpackungsmaterial, Produktverpackung, Versandverpackung, Stammdaten→Lieferanten/Kategorien); View-Titel; Lieferant Default-Land DE; PackagingItem URL-Feld + Kategorie-Split. |
|
||
| 10.04.2026 | **5.1b** | Feedback Rezeptur & INCI: Ingredient `material_quality_id` + Anzeige im Produktformular; Rezeptur Gramm→Prozent (3 Nachkomma) mit 100%-Summe + Warnung; Hersteller-Rezeptur Kasten. |
|
||
| 10.04.2026 | **5.1c** | Feedback Produktion: edit/update + copy-Funktion; nur aktive Produkte; Default-Lagerort Koeln. |
|
||
|| | 6–9 | *noch offen* |
|
||
|
||
*Hinweis:* Diese Tabelle bei jeder abgeschlossenen Teil-Lieferung um eine Zeile ergaenzen.
|
||
|
||
---
|
||
|
||
## Uebersicht der Phasen
|
||
|
||
| Phase | Titel | Status | Geschaetzte Dauer | Abhaengigkeit |
|
||
|-------|-------|--------|-------------------|---------------|
|
||
| **0** | Bugfixes & Vorbereitung | **erledigt** (27.03.2026) | 2-3 Tage | - |
|
||
| **1** | INCI-Management Erweiterung | **erledigt** (27.03.2026) | 4-5 Tage | Phase 0 |
|
||
| **2** | Stammdaten-Module | **umgesetzt** (CRUD/Stammdaten, 27.03.2026) | 5-6 Tage | - |
|
||
| **3** | Wareneingang | **umgesetzt** (27.03.2026) | 5-7 Tage | Phase 2 |
|
||
| **4** | Rezeptur, BOM & Produkterweiterung | **umgesetzt** (27.03.2026) | 3-4 Tage | Phase 1 + 2 |
|
||
| **5** | Produktion & Bestandsabzug | **umgesetzt** (MVP 27.03.2026) | 6-8 Tage | Phase 3 + 4 |
|
||
| **5.1** | Feedback-Korrekturen (Menue, Rezeptur, Produktion) | **umgesetzt** (10.04.2026) | 5-7 Tage | Phase 1-5 |
|
||
| **6** | Bestandsuebersicht & Alarme | offen | 4-5 Tage | Phase 3 + 5 |
|
||
| **7** | Ausgang / Ausschuss | offen | 2-3 Tage | Phase 2 + 6 |
|
||
| **8** | Rollen, Audit-Trail & Feinschliff | offen | 3-4 Tage | Alle Phasen |
|
||
| **9** | Einstellungen & Konfiguration | offen | 1-2 Tage | Phase 6 |
|
||
|
||
```
|
||
Phase 0 --> Phase 1 --> Phase 4 --> Phase 5 --> Phase 6 --> Phase 8
|
||
/ / /
|
||
Phase 2 --> Phase 3 ------------- Phase 7 + 9 -------
|
||
```
|
||
|
||
---
|
||
|
||
## Phase 0: Bugfixes & Vorbereitung (2-3 Tage)
|
||
|
||
> **Ziel:** Solide Grundlage schaffen, bestehende Fehler beseitigen.
|
||
>
|
||
> **Status:** **Abgeschlossen** (Umsetzung 27.03.2026)
|
||
|
||
### Aufgaben
|
||
|
||
#### 0.1 - Bugfix: Product/Ingredient hasMany-Relationen — [x] erledigt
|
||
- **Dateien:** `app/Models/Product.php`, `app/Models/Ingredient.php`
|
||
- **Problem:** `product_ingredients()` hasMany hatte einen falschen Foreign-Key-Namen (`'product_ingredients'` / `'id'` statt echter FK-Spalte).
|
||
- **Ist-Umsetzung:** `hasMany(ProductIngredient::class, 'product_id')` bzw. `hasMany(..., 'ingredient_id')`.
|
||
|
||
#### 0.2 - Bugfix: updateIngredients() Sync-Logik — [x] erledigt
|
||
- **Datei:** `app/Repositories/ProductRepository.php`
|
||
- **Problem:** Nur Hinzufuegen, kein Entfernen von INCIs.
|
||
- **Ist-Umsetzung:** `$this->model->p_ingredients()->sync($ids)` mit gefilterten Integer-IDs (nur positive IDs).
|
||
|
||
#### 0.3 - Bugfix: Alphabetische Sortierung Dropdown — [x] erledigt
|
||
- **Datei:** `app/Services/HTMLHelper.php` (Klasse `HTMLHelper` liegt im Projekt unter **Services**, nicht unter `Libraries`)
|
||
- **Ist-Umsetzung:** `Ingredient::where('active', 1)->orderBy('name')->get()` in `getProductIngredientsOptions()`.
|
||
|
||
#### 0.4 - Bugfix: Attribute-Kopierung — [x] erledigt
|
||
- **Datei:** `app/Repositories/ProductRepository.php` (Methode `copy()`)
|
||
- **Ist-Umsetzung:** `type_id` aus der Quell-Zeile `ProductAttribute`: `$attribute->type_id` (statt nicht existierendem Feld am `Product`).
|
||
|
||
#### 0.5 - Migrationskorrekturen (Tests / Schema-Sauberkeit) — [x] erledigt
|
||
> Nicht im urspruenglichen Plan; noetig damit `php artisan test` mit SQLite (In-Memory) und frische Installationen konsistent laufen.
|
||
|
||
| Migration | Problem | Ist-Umsetzung |
|
||
|-----------|---------|---------------|
|
||
| `2018_09_29_145909_create_countries_table.php` | Spalte `active` war doppelt definiert | Duplikat entfernt |
|
||
| `2021_06_15_112357_create_product_buys_table.php` | FK auf `user_id`, Spalte heisst `auth_user_id` | FK auf `auth_user_id` korrigiert |
|
||
|
||
*Hinweis fuer bestehende Produktions-DBs:* Diese Dateien wurden bei laufenden Systemen typischerweise schon einmal migriert; die Korrekturen gelten vor allem fuer **neue** Deployments und die Test-Pipeline.
|
||
|
||
### Tests — [x] umgesetzt
|
||
|
||
**Datei:** `tests/Feature/ProductPhase0Test.php` (Pest)
|
||
|
||
| Geplanter Aspekt | Ist-Abdeckung |
|
||
|------------------|---------------|
|
||
| Relation `Product` → `product_ingredients` | Test: korrekte FK-Verknuepfung |
|
||
| Relation `Ingredient` → `product_ingredients` | Test: korrekte FK-Verknuepfung |
|
||
| INCIs entfernen (Sync) | Test: `updateIngredients` mit zwei, dann einer ID |
|
||
| Dropdown alphabetisch | Test: Reihenfolge Alpha vor Zebra im HTML-Output |
|
||
| Produktkopie Attribute | Test: `type_id` der `ProductAttribute`-Zeile bleibt erhalten |
|
||
|
||
Nicht als separater HTTP-Feature-Test umgesetzt (optional spaeter): Formular-POST `admin/product/store` mit INCI-Auswahl — funktional durch Repository-Sync abgedeckt.
|
||
|
||
### Deliverable
|
||
|
||
Bereinigte Codebasis ohne die in Phase 0 adressierten Bugs im Produkt/INCI-Bereich; automatisierte Regressionstests fuer die genannten Punkte.
|
||
|
||
---
|
||
|
||
## Phase 1: INCI-Management Erweiterung (4-5 Tage)
|
||
|
||
> **Ziel:** INCI-Verwaltung laut Briefing: Sortierung, Gramm, Faktor, Haltbarkeit, Kopierfunktion.
|
||
>
|
||
> **Status:** **Abgeschlossen** (Umsetzung 27.03.2026)
|
||
>
|
||
> **Vorgaben Umsetzung (abgestimmt):** SortableJS per **CDN** in `resources/views/admin/product/edit.blade.php` (`@section('scripts')`); **Select2** fuer Einzel-Auswahl „Inhaltsstoff hinzufuegen“ (Zeilen werden per JS angehaengt; Reihenfolge per Drag & Drop).
|
||
|
||
### Aufgaben
|
||
|
||
#### 1.1 - Migration: product_ingredients erweitern — [x] erledigt
|
||
- **Datei:** `database/migrations/2026_03_27_111352_add_recipe_fields_to_product_ingredients_table.php`
|
||
- Spalten: `pos`, `gram` (nullable), `factor` (Default 1.10)
|
||
|
||
#### 1.2 - Migration: ingredients erweitern — [x] erledigt
|
||
- **Datei:** `database/migrations/2026_03_27_111354_add_inventory_fields_to_ingredients_table.php`
|
||
- Spalten: `default_factor`, `min_stock_alert`
|
||
|
||
#### 1.3 - Migration: products erweitern (Haltbarkeit) — [x] erledigt
|
||
- **Datei:** `database/migrations/2026_03_27_111353_add_shelf_life_to_products_table.php`
|
||
- Spalten: `shelf_life_type` (ENUM `pao`/`fixed`, nullable), `shelf_life_months` (nullable)
|
||
|
||
#### 1.4 - Model: ProductIngredient aktualisieren — [x] erledigt
|
||
- `fillable` + `casts` fuer `pos`, `gram`, `factor`
|
||
- `Product::p_ingredients()` / `Ingredient::products()`: `withPivot('id','pos','gram','factor')`, `orderByPivot('pos')`
|
||
|
||
#### 1.5 - Model: Ingredient aktualisieren — [x] erledigt
|
||
- `fillable` + `casts`; **UI:** `resources/views/admin/ingredient/form.blade.php` + Speicherlogik in `IngredientController::store()` (Zahlen mit `reFormatNumber`)
|
||
|
||
#### 1.6 - Frontend: SortableJS Drag & Drop — [x] erledigt
|
||
- **Datei:** `resources/views/admin/product/edit.blade.php` (CDN SortableJS 1.15.2), Ziel `#ingredient-sortable-rows`
|
||
|
||
#### 1.7 - Frontend: Select2 — [x] erledigt
|
||
- Einzel-Select `#ingredient-add-select`; Katalog kommt aus `ProductController::edit()` als `ingredient_catalog` (alphabetisch)
|
||
|
||
#### 1.8 - Frontend: Gramm & Faktor Felder — [x] erledigt
|
||
- Pro Zeile `pi_gram[]`, `pi_factor[]`; Effektiv-Spalte per jQuery; Default-Faktor beim Hinzufuegen aus JSON-Katalog (`default_factor`), **ohne** zusaetzlichen AJAX-Endpoint
|
||
|
||
#### 1.9 - Frontend: Haltbarkeits-Auswahl — [x] erledigt
|
||
- Karte **Warenwirtschaft** in `admin/product/form.blade.php`: Radios keine Angabe / PAO / festes MHD; Monats-Dropdown nur bei „festes MHD“ (JS-Toggle)
|
||
|
||
#### 1.10 - Backend: ProductRepository aktualisieren — [x] erledigt
|
||
- `updateIngredients()`: parallele Arrays `pi_ingredient_id`, `pi_gram`, `pi_factor`; `sync()` mit Pivot; **Hidden** `product_inci_sync_sent`: verhindert, dass Speichern ohne INCI-Bereich alle INCIs loescht; Legacy-Pfad `product_ingredients[]` (ohne Hidden) fuer Tests/Altkunden
|
||
- `update()`: Normalisierung `shelf_life_type` / `shelf_life_months`
|
||
- `copy()`: kopiert `pos`, `gram`, `factor` sowie Haltbarkeit (via `replicate()`)
|
||
|
||
### Tests — [x] umgesetzt
|
||
|
||
**Dateien:** `tests/Feature/ProductPhase1Test.php`, ergaenzend `ProductPhase0Test.php`
|
||
|
||
| Geplanter Aspekt | Ist-Abdeckung |
|
||
|------------------|---------------|
|
||
| Gramm/Faktor/Haltbarkeit speichern | Test mit `product_inci_sync_sent` + `reFormatNumber`-Eingaben |
|
||
| Produkt-Kopie pos/gram/factor (+ PAO) | Test `ProductPhase1Test` zweiter Fall |
|
||
|
||
Nicht separat als E2E-Test: reine Reihenfolge nur Drag & Drop im Browser (Speicherung erfolgt ueber Reihenfolge der `pi_*`-Arrays).
|
||
|
||
### Deliverable
|
||
|
||
Erweitertes INCI-Management mit Rezeptur-Spalten, sortierbarer Tabelle, Haltbarkeit am Produkt und angepasstem Inhaltsstoff-Stammdaten-Formular.
|
||
|
||
---
|
||
|
||
## Phase 2: Stammdaten-Module (5-6 Tage)
|
||
|
||
> **Ziel:** Alle Stammdaten-Tabellen und CRUD-Oberflaechen fuer die Warenwirtschaft.
|
||
>
|
||
> **Status:** **Stammdaten-CRUD & Rechte** umgesetzt (27.03.2026). Routen liegen in `routes/web.php` innerhalb der bestehenden `auth:user`-Gruppe, verschachtelt mit `superadmin` bzw. `admin` (analog zum Plan, nur ohne separaten aeusseren `auth:user`-Wrapper).
|
||
|
||
### Aufgaben
|
||
|
||
#### 2.1 - Modelle & Migrations erstellen
|
||
```bash
|
||
php artisan make:model Location -mf --no-interaction
|
||
php artisan make:model SupplierCategory -mf --no-interaction
|
||
php artisan make:model Supplier -mf --no-interaction
|
||
php artisan make:model MaterialQuality -mf --no-interaction
|
||
php artisan make:model PackagingMaterial -mf --no-interaction
|
||
php artisan make:model PackagingItem -mf --no-interaction
|
||
```
|
||
- Tabellenstrukturen laut `konzept-final.md` Abschnitt 3
|
||
- Factories mit sinnvollen Faker-Daten
|
||
- **Pivot-Tabelle** `supplier_supplier_category` fuer n:m Lieferant-Kategorie-Beziehung
|
||
- Supplier: `belongsToMany(SupplierCategory::class)` statt `belongsTo`
|
||
|
||
#### 2.2 - Seeder: Vorbelegte Stammdaten
|
||
```bash
|
||
php artisan make:seeder InventoryStammdatenSeeder --no-interaction
|
||
```
|
||
- Lagerorte: Koeln, Waldboel
|
||
- Qualitaeten: konventionell, bio kaltgepresst, bio raffiniert, konventionell kaltgepresst, konventionell raffiniert
|
||
- Verpackungs-Materialien: Glas, Holz/Bambus, Pappe/Papier, Kunststoff
|
||
|
||
#### 2.3 - Repositories erstellen
|
||
- `SupplierRepository` - CRUD fuer Lieferanten inkl. Kategorie-Sync (n:m)
|
||
- `PackagingItemRepository` - CRUD fuer Verpackungs-Artikel
|
||
- Einfache Stammdaten (Locations, Qualitaeten etc.) koennen direkt im Controller verarbeitet werden (kein eigenes Repository noetig bei reinem CRUD)
|
||
|
||
#### 2.4 - Controller & FormRequests
|
||
```bash
|
||
php artisan make:controller Admin/Inventory/LocationController --resource --no-interaction
|
||
php artisan make:controller Admin/Inventory/SupplierCategoryController --resource --no-interaction
|
||
php artisan make:controller Admin/Inventory/SupplierController --resource --no-interaction
|
||
php artisan make:controller Admin/Inventory/MaterialQualityController --resource --no-interaction
|
||
php artisan make:controller Admin/Inventory/PackagingMaterialController --resource --no-interaction
|
||
php artisan make:controller Admin/Inventory/PackagingItemController --resource --no-interaction
|
||
```
|
||
- Pro Controller eine `StoreRequest` und `UpdateRequest` FormRequest-Klasse
|
||
- SupplierController nutzt `SupplierRepository`
|
||
|
||
#### 2.5 - Routes
|
||
- Neue Route-Gruppe in `routes/web.php`:
|
||
```php
|
||
// Warenwirtschaft - Stammdaten (SuperAdmin)
|
||
Route::middleware(['auth:user', 'superadmin'])->prefix('admin/inventory')->group(function () {
|
||
Route::resource('locations', LocationController::class);
|
||
Route::resource('material-qualities', MaterialQualityController::class);
|
||
Route::resource('packaging-materials', PackagingMaterialController::class);
|
||
});
|
||
|
||
// Warenwirtschaft - Stammdaten (Admin)
|
||
Route::middleware(['auth:user', 'admin'])->prefix('admin/inventory')->group(function () {
|
||
Route::resource('supplier-categories', SupplierCategoryController::class);
|
||
Route::resource('suppliers', SupplierController::class);
|
||
Route::resource('packaging-items', PackagingItemController::class);
|
||
});
|
||
```
|
||
|
||
#### 2.6 - Blade Views
|
||
- Fuer jede Stammdaten-Entitaet: `index.blade.php` (DataTable) + `form.blade.php` (Create/Edit)
|
||
- Pfad: `resources/views/admin/inventory/`
|
||
- Layout: `@extends('layouts.layout-2')` (konsistent mit bestehendem Admin)
|
||
- Styling: Bootstrap 4 (konsistent)
|
||
- DataTables: jQuery DataTables (konsistent)
|
||
- Supplier-Formular: Select2 Mehrfachauswahl fuer Kategorien
|
||
|
||
#### 2.7 - Menue-Integration (Sidenav)
|
||
- **Datei:** `resources/views/layouts/includes/layout-sidenav.blade.php`
|
||
- Neuer Menue-Block "Warenwirtschaft" nach "Produkte"
|
||
- Bedingte Anzeige je nach Rolle
|
||
- Zunaechst nur Stammdaten-Links (weitere in spaeteren Phasen)
|
||
|
||
### Tests
|
||
- [x] `tests/Feature/InventoryPhase2Test.php`: je Stammdaten-Entitaet **CRUD** (Index/Create/Store/Edit/Update/Destroy) mit passender Rolle (SuperAdmin bzw. Admin); Lieferant inkl. **n:m**-Kategorien und Soft-Delete; Verpackungsartikel mit Produkt-/Lieferanten-Zuordnung und Leeren optionaler FKs beim Update; zusaetzlich **Rechte** (Admin kein SuperAdmin-Inventory, CopyReader kein Lieferanten-Index, Admin kein `locations.store`).
|
||
|
||
### Deliverable
|
||
Alle Stammdaten-Module mit vollstaendiger CRUD-Funktionalitaet und korrekter Rechtevergabe.
|
||
|
||
---
|
||
|
||
## Phase 3: Wareneingang (5-7 Tage)
|
||
|
||
> **Ziel:** Zweistufiges Eingangs-System (Pending -> Received).
|
||
|
||
### Aufgaben
|
||
|
||
#### 3.1 - Model & Migration
|
||
```bash
|
||
php artisan make:model StockEntry -mf --no-interaction
|
||
```
|
||
- Tabellenstruktur laut `konzept-final.md`
|
||
- Inkl. `unit` ENUM('gram','piece') Spalte
|
||
|
||
#### 3.2 - Repository
|
||
- `StockEntryRepository` - CRUD + Stufe-2-Buchung (`receive()`)
|
||
- Methoden: `create()`, `update()`, `receive()`, `getByStatus()`, `getForIngredient()`
|
||
|
||
#### 3.3 - Controller & FormRequests
|
||
```bash
|
||
php artisan make:controller Admin/Inventory/StockEntryController --resource --no-interaction
|
||
```
|
||
- `StoreStockEntryRequest` - Validierung Stufe 1 (Einkauf)
|
||
- `ReceiveStockEntryRequest` - Validierung Stufe 2 (Eingang)
|
||
- Separate Route/Methode `receive()` fuer die Stufe-2-Buchung
|
||
|
||
#### 3.4 - Routes
|
||
```php
|
||
Route::middleware(['auth:user', 'copyreader'])->prefix('admin/inventory')->group(function () {
|
||
Route::resource('stock-entries', StockEntryController::class);
|
||
Route::put('stock-entries/{stockEntry}/receive', [StockEntryController::class, 'receive'])
|
||
->name('stock-entries.receive');
|
||
});
|
||
```
|
||
> **Hinweis:** Einkauf anlegen (`store`) wird ueber zusaetzliche Autorisierung auf `admin` beschraenkt, da CopyReader (Ivonne) nur Stufe 2 (buchen) darf. Umsetzung im Controller via `$this->authorize()` oder Gate.
|
||
|
||
#### 3.5 - Views
|
||
- `index.blade.php` - Liste aller Eingaenge mit Status-Badges (orange/gruen)
|
||
- `create.blade.php` - Stufe-1-Formular (dynamische Felder je nach Kategorie via JS)
|
||
- `show.blade.php` - Detail-Ansicht + Stufe-2-Formular (Eingangsbuchung)
|
||
- Inhaltsstoff-Feld: Select2 Autosuggest (AJAX-Suche)
|
||
|
||
#### 3.6 - API-Endpoint: Ingredienten-Suche
|
||
- Route: `GET /admin/inventory/api/ingredients/search?q=shea`
|
||
- Gibt JSON mit matching Ingredients zurueck
|
||
- Fuer Select2 AJAX-Anbindung
|
||
|
||
### Tests
|
||
- [x] Feature-Test: Stufe 1 - Einkauf erstellen (Status pending) — `InventoryPhase3Test.php`
|
||
- [x] Feature-Test: Stufe 2 - Wareneingang buchen (Status received)
|
||
- [x] Feature-Test: unit wird korrekt gesetzt (gram/piece)
|
||
- [x] Feature-Test: CopyReader kann Stufe 2, aber nicht Stufe 1 (Create-Form Redirect)
|
||
- [x] Feature-Test: Admin kann Stufe 1 (store)
|
||
- [x] Feature-Test: Sortierung: Pending oben, dann Received (`listForIndex`)
|
||
|
||
### Deliverable
|
||
Funktionierender Wareneingang mit Zwei-Stufen-Workflow und rollenbasiertem Zugriff.
|
||
|
||
---
|
||
|
||
## Phase 4: Rezeptur, BOM & Produkterweiterung (3-4 Tage)
|
||
|
||
> **Ziel:** Verpackung am Produkt zuweisen (Bill of Materials).
|
||
>
|
||
> **Status:** **Umgesetzt** (27.03.2026) – Pivot `product_packagings` mit `pos`; UI wie INCI (Modal + SortableJS, kein separates Select2-Feld).
|
||
|
||
### Aufgaben
|
||
|
||
#### 4.1 - Migration: product_packagings
|
||
```bash
|
||
php artisan make:model ProductPackaging -m --no-interaction
|
||
```
|
||
- Spalten: `product_id`, `packaging_item_id`, `quantity` (Default 1)
|
||
|
||
#### 4.2 - Model-Relationen
|
||
- `Product::packagings()` -> `belongsToMany(PackagingItem::class, 'product_packagings')->withPivot('quantity')`
|
||
- `PackagingItem::products()` -> inverse Relation
|
||
|
||
#### 4.3 - Frontend: Produkt-Formular erweitern
|
||
- **Datei:** `resources/views/admin/product/form.blade.php`
|
||
- Neuer Abschnitt "Verpackung & Material" nach den INCIs
|
||
- Select2-Dropdown: Packaging-Artikel auswaehlen
|
||
- Pro Zeile: Artikelname, Material (readonly), Gewicht (readonly), Menge (editierbar)
|
||
- Sortierbar per SortableJS
|
||
|
||
#### 4.4 - Backend: ProductRepository erweitern
|
||
- Neue Methode `updatePackagings($data)` - Sync-Logik fuer `product_packagings`
|
||
- `copy()` erweitern: Packaging-Zuweisungen mitkopieren
|
||
|
||
### Tests
|
||
- [x] Feature-Test: Packaging am Produkt zuweisen und speichern — `ProductPhase4Test.php`
|
||
- [x] Feature-Test: Produkt-Kopie uebernimmt Packaging
|
||
- [x] Feature-Test: Menge pro Packaging-Artikel wird gespeichert
|
||
|
||
### Deliverable
|
||
Produkte haben eine vollstaendige Stueckliste (Rezeptur + Verpackung).
|
||
|
||
---
|
||
|
||
## Phase 5: Produktion & Bestandsabzug (6-8 Tage)
|
||
|
||
> **Ziel:** Produktionslauf erfassen, Chargen zuordnen, Bestaende abziehen.
|
||
>
|
||
> **Status:** **MVP umgesetzt** (27.03.2026). Bestandsabzug ist rechnerisch ueber gespeicherte Verbrauchswerte vorbereitet; vollstaendige `InventoryService`-Bilanz folgt Phase 6.
|
||
|
||
### Aufgaben
|
||
|
||
#### 5.1 - Modelle & Migrations
|
||
```bash
|
||
php artisan make:model Production -mf --no-interaction
|
||
php artisan make:model ProductionIngredient -m --no-interaction
|
||
php artisan make:model ProductionPackaging -m --no-interaction
|
||
```
|
||
- `production_packagings`: Snapshot-Tabelle fuer Packaging-Verbrauch pro Produktion
|
||
(Spalten: `production_id`, `packaging_item_id`, `quantity_used`)
|
||
|
||
#### 5.2 - Repository & Service
|
||
- `ProductionRepository` - CRUD fuer Produktions-Eintraege
|
||
- `ProductionService` - Orchestriert den Produktions-Workflow:
|
||
1. `production_ingredients`-Eintraege erstellen
|
||
2. `production_packagings`-Snapshot erstellen
|
||
3. MHD-Check: Warnung wenn Rohstoff-MHD < Produkt-MHD
|
||
- Transaktionssicher (`DB::transaction()`)
|
||
|
||
#### 5.3 - Controller & FormRequests
|
||
```bash
|
||
php artisan make:controller Admin/Inventory/ProductionController --resource --no-interaction
|
||
```
|
||
- `StoreProductionRequest` - Validierung aller Felder inkl. Chargen-Zuordnung
|
||
|
||
#### 5.4 - Routes
|
||
```php
|
||
Route::middleware(['auth:user', 'copyreader'])->prefix('admin/inventory')->group(function () {
|
||
Route::resource('productions', ProductionController::class);
|
||
});
|
||
```
|
||
|
||
#### 5.5 - AJAX-API: Produkt-Daten laden
|
||
- Route: `GET /admin/inventory/api/products/{id}/recipe`
|
||
- Gibt JSON zurueck: INCIs (mit Gramm, Faktor), Packagings, letzte 3 Chargen pro INCI am Standort
|
||
|
||
#### 5.6 - Views
|
||
- `index.blade.php` - Liste vergangener Produktionen (DataTable)
|
||
- `create.blade.php` - Produktions-Formular:
|
||
- Produkt-Dropdown
|
||
- Datum (Default heute)
|
||
- Stueckzahl
|
||
- Standort-Dropdown
|
||
- INCI-Liste (automatisch geladen): Pro INCI Chargen-Dropdown (letzte 3, Mehrfachauswahl)
|
||
- Packaging-Liste (automatisch geladen, read-only)
|
||
- `show.blade.php` - Detail-Ansicht einer Produktion
|
||
|
||
#### 5.7 - Chargen-Splitting UI
|
||
- Wenn ein User zwei Chargen fuer denselben INCI waehlt, erscheinen zwei Mengen-Felder
|
||
- Validierung: Summe der Teilmengen = Gesamtverbrauch (gram x factor x stueckzahl)
|
||
|
||
### Tests
|
||
- [x] Feature-Test: Produktion erstellen mit Chargen-Zuordnung — `ProductionPhase5Test.php`
|
||
- [ ] Feature-Test: Bestandsabzug nach Produktion korrekt berechnet (Rohstoffe) — Phase 6 / `InventoryService`
|
||
- [x] Feature-Test: Packaging-Snapshot wird korrekt erstellt
|
||
- [ ] Feature-Test: Chargen-Splitting (2 Chargen fuer 1 INCI) — UI unterstuetzt „Weitere Charge“; separater Assertions-Test optional
|
||
- [x] Feature-Test: MHD-Warnung wird ausgeloest
|
||
- [ ] Feature-Test: Packaging wird korrekt abgezogen — Bestandslogik Phase 6
|
||
- [ ] Feature-Test: BOM-Aenderung nach Produktion aendert historische Bestaende nicht — Snapshot-Tabelle abgedeckt; Integrations-Test optional
|
||
- [x] Unit-/Service-Test: ProductionService (Soll/Ist, API-Rezept)
|
||
|
||
### Deliverable
|
||
Vollstaendiger Produktions-Workflow mit Chargen-Tracking, Packaging-Snapshots und automatischem Bestandsabzug.
|
||
|
||
---
|
||
|
||
## Phase 5.1: Feedback-Korrekturen (5-7 Tage)
|
||
|
||
> **Ziel:** Alle Kundenfeedback-Punkte aus `feedback.md` umsetzen, bevor Phase 6 beginnt.
|
||
>
|
||
> **Status:** **Umgesetzt** (10.04.2026)
|
||
>
|
||
> **Quelle:** Feedback der Kundin (April 2026) zu den bisherigen Umsetzungen Phase 0-5.
|
||
|
||
### 5.1a — Menue & Stammdaten-Korrekturen
|
||
|
||
#### 5.1a.1 — Menustruktur Sidenav umbenennen — [x] erledigt
|
||
- **Datei:** `resources/views/layouts/includes/layout-sidenav.blade.php`
|
||
- Aenderungen:
|
||
- „Materialqualitaeten" → „Rohstoffqualitaet"
|
||
- „Verpackungsmaterialien" → „Verpackungsmaterial" (Singular)
|
||
- Admin-Untermenue „Stammdaten" → „Lieferanten"; erster Punkt „Lieferanten-Kategorien" → „Kategorien"
|
||
- „Verpackungsartikel" → „Produktverpackung"
|
||
- Neuer Menuepunkt „Versandverpackung" (filtert auf Kategorie `shipping`)
|
||
|
||
#### 5.1a.2 — View-Titel anpassen — [x] erledigt
|
||
- **Dateien:** Views unter `resources/views/admin/inventory/`
|
||
- `material-qualities/index.blade.php` + `form.blade.php`: „Materialqualitaet" → „Rohstoffqualitaet"
|
||
- `packaging-materials/index.blade.php` + `form.blade.php`: „Verpackungsmaterialien" → „Verpackungsmaterial"
|
||
- `packaging-items/index.blade.php` + `form.blade.php`: „Verpackungsartikel" → „Produktverpackung" / „Versandverpackung"
|
||
|
||
#### 5.1a.3 — PackagingItem Kategorien bereinigen — [x] erledigt
|
||
- **Problem:** Aktuell ENUM `packaging|label|shipping_office`. Feedback: „Etikett" und „Versand/Buero" gehoeren nicht unter Produktverpackung.
|
||
- **Loesung:** Kategorien aufteilen in zwei Gruppen:
|
||
- Produktverpackung: `packaging` (nur Verpackungsartikel fuer das Produkt)
|
||
- Versandverpackung: `shipping` (Versand- & Bueromaterial, Etiketten)
|
||
- **Migration:** ENUM aendern auf `packaging|shipping`; bestehende `label` und `shipping_office` auf `shipping` migrieren
|
||
- **Routing:** Separate Index-Routen fuer Produktverpackung (`?category=packaging`) und Versandverpackung (`?category=shipping`)
|
||
|
||
#### 5.1a.4 — PackagingItem URL-Feld — [x] erledigt
|
||
- **Migration:** Spalte `url` (nullable string) an `packaging_items` anfuegen
|
||
- **Model:** `url` in `$fillable`
|
||
- **Formular:** Neues Textfeld „URL (Onlineshop)" in `packaging-items/form.blade.php`
|
||
- **Validierung:** `nullable|url|max:500`
|
||
|
||
#### 5.1a.5 — Lieferant: Default Land Deutschland — [x] erledigt
|
||
- **Datei:** `resources/views/admin/inventory/suppliers/form.blade.php`
|
||
- Default-Wert fuer `country_id` beim Erstellen: ID von Deutschland (`Country::where('code', 'DE')->first()->id`)
|
||
- Controller `create()`: `$defaultCountryId` mitgeben
|
||
|
||
#### 5.1a.6 — Verpackungsartikel Produkt-Zuordnung — [x] erledigt
|
||
- **Feedback:** Dropdown unten (Produkt) ist optional; wenn behalten → Mehrfachauswahl
|
||
- **Loesung:** `product_id` (FK Singular) durch n:m-Relation ersetzen ODER komplett entfernen
|
||
- **Empfehlung:** Entfernen, da Produkt-Verpackungs-Zuordnung bereits ueber `product_packagings` (Phase 4) abgebildet ist; `product_id` am PackagingItem ist redundant
|
||
|
||
### 5.1b — Rezeptur & INCI-Korrekturen
|
||
|
||
#### 5.1b.1 — INCI: Qualitaet (material_quality_id) — [x] erledigt
|
||
- **Migration:** Spalte `material_quality_id` (nullable FK) an `ingredients`-Tabelle
|
||
- **Model:** `Ingredient` erhaelt `belongsTo(MaterialQuality::class)` Relation; `material_quality_id` in `$fillable`
|
||
- **Formular:** `resources/views/admin/ingredient/form.blade.php` — neues Select2-Dropdown „Rohstoffqualitaet" (Daten aus `material_qualities`)
|
||
- **Produktformular:** Modal + Select2 zeigt dritte Spalte „Qualitaet" bei der INCI-Auswahl (z.B. „Sonnenblumenoel — bio kaltgepresst")
|
||
- **Katalog JSON:** `ingredient_catalog` in `ProductController::edit()` um `quality_name` erweitern
|
||
- **Anzeige Rezeptur-Tabelle:** Qualitaet als Spalte neben INCI-Name anzeigen
|
||
|
||
#### 5.1b.2 — Rezeptur: Gramm → Prozent (3 Nachkommastellen) — [x] erledigt
|
||
- **Migration:** Spalte `gram` in `product_ingredients` umbenennen zu `percentage` (decimal 8,3)
|
||
- **Model:** `ProductIngredient` Cast + Fillable anpassen
|
||
- **Frontend:** Label „Gramm" → „Anteil (%)", Input mit `step="0.001"`, 3 Nachkommastellen
|
||
- **Effektiv-Berechnung:** `Effektiv = Prozent × Faktor`
|
||
- **Summenzeile:** Am Ende der Rezeptur-Tabelle eine Zeile „Gesamt" mit Summe aller Prozentwerte
|
||
- Wenn Summe == 100.000 → gruen
|
||
- Wenn Summe != 100.000 → rot + Hinweis „Die Gesamtrezeptur ergibt nicht 100 %!"
|
||
- **Backend:** `ProductRepository::updateIngredients()` + Validierung anpassen
|
||
|
||
#### 5.1b.3 — Hersteller-Rezeptur (zweiter Kasten) — [x] erledigt
|
||
- **Konzept:** Zweiter identischer INCI-Kasten im Produktformular fuer die „Hersteller-Rezeptur"
|
||
- **Migration:** Neue Spalte `recipe_type` ENUM(`product`, `manufacturer`) an `product_ingredients` mit Default `product`
|
||
- **Model:** Zwei Scopes am `Product`: `productIngredients()` und `manufacturerIngredients()` mit Filter auf `recipe_type`
|
||
- **Frontend:** Zweite sortierbare Tabelle „Hersteller Rezeptur" unter der Produkt-Rezeptur (gleiche Funktionalitaet: Drag & Drop, Prozent, Faktor, 100%-Summe)
|
||
- **Backend:** `ProductRepository::updateIngredients()` verarbeitet beide Rezepturen getrennt (separate Hidden-Felder `manufacturer_inci_sync_sent`)
|
||
- **Kopie:** `copy()` kopiert beide Rezepturen
|
||
|
||
### 5.1c — Produktion-Korrekturen
|
||
|
||
#### 5.1c.1 — Produktion bearbeitbar (edit/update) — [x] erledigt
|
||
- **Controller:** `ProductionController::edit()` und `update()` implementieren
|
||
- **FormRequest:** `UpdateProductionRequest` erstellen
|
||
- **View:** `resources/views/admin/inventory/productions/edit.blade.php` (analog create, vorbelegt)
|
||
- **Route:** `Route::resource('productions', ...)` deckt edit/update bereits ab (wenn Standard-Resource)
|
||
- **Hinweis:** Bereits abgeschlossene Produktionen editierbar machen (Fehlerkorrektur)
|
||
|
||
#### 5.1c.2 — Produktion kopierbar — [x] erledigt
|
||
- **Controller:** `ProductionController::copy(Production $production)` → Zeigt create-Formular vorbelegt mit Daten der Quell-Produktion (Produkt, Standort, Chargen-Zuordnungen)
|
||
- **Route:** `GET admin/inventory/productions/{production}/copy`
|
||
- **View:** Nutzt `create.blade.php` mit vorbelegten Werten
|
||
- **Use Case:** Gleiche Rezeptur fuer verschiedene Groessen (z.B. Deobalm 50ml → 5ml)
|
||
|
||
#### 5.1c.3 — Nur aktive Produkte im Dropdown — [x] erledigt
|
||
- **Controller:** `ProductionController::create()` + `edit()`: `Product::where('active', 1)->get()`
|
||
- **API:** `recipeJson()` auch fuer inaktive zulassen (fuer bestehende Produktionen)
|
||
|
||
#### 5.1c.4 — Default Lagerort Koeln — [x] erledigt
|
||
- **Controller:** `ProductionController::create()`: Default `location_id` = Koeln (aus `locations`-Tabelle)
|
||
- **View:** Dropdown vorselektiert
|
||
|
||
### Tests Phase 5.1
|
||
|
||
- [ ] Feature-Test: Menue-Labels korrekt angezeigt (Rohstoffqualitaet etc.)
|
||
- [ ] Feature-Test: PackagingItem mit URL speichern
|
||
- [ ] Feature-Test: Lieferant Default-Land = DE
|
||
- [ ] Feature-Test: INCI mit Qualitaet speichern + Anzeige im Produktformular
|
||
- [ ] Feature-Test: Rezeptur in Prozent speichern (3 Nachkomma)
|
||
- [ ] Feature-Test: 100%-Summen-Validierung
|
||
- [ ] Feature-Test: Hersteller-Rezeptur separat speichern
|
||
- [ ] Feature-Test: Produktion bearbeiten (edit/update)
|
||
- [ ] Feature-Test: Produktion kopieren
|
||
- [ ] Feature-Test: Nur aktive Produkte im Dropdown
|
||
|
||
### Deliverable
|
||
|
||
Alle Feedback-Punkte der Kundin umgesetzt; Menustruktur, Rezeptur und Produktion entsprechen den Erwartungen.
|
||
|
||
---
|
||
|
||
## Phase 6: Bestandsuebersicht & Alarme (4-5 Tage)
|
||
|
||
> **Ziel:** Vier Bestandsseiten mit Echtzeit-Berechnung und Alarm-System.
|
||
|
||
### Aufgaben
|
||
|
||
#### 6.1 - Service: InventoryService
|
||
- **Datei:** `app/Services/InventoryService.php`
|
||
- Methoden:
|
||
- `getIngredientStock(?int $locationId = null)` - Rohstoff-Bestaende
|
||
- `getPackagingStock(string $category, ?int $locationId = null)` - Packaging/Versand
|
||
- `getItemsBelowThreshold()` - Alle Artikel unter Meldebestand
|
||
- `calculateDynamicThreshold(int $ingredientId)` - **NEU (Feedback):** Automatische Berechnung
|
||
- Bestandsberechnung Rohstoffe: `SUM(stock_entries.received_quantity) - SUM(production_ingredients.quantity_used) - SUM(stock_disposals.quantity)`
|
||
- Bestandsberechnung Packaging: `SUM(stock_entries.received_quantity) - SUM(production_packagings.quantity_used) - SUM(stock_disposals.quantity)`
|
||
|
||
##### 6.1.1 - Dynamische Meldebestands-Berechnung (Feedback)
|
||
- **Algorithmus:** Durchschnittlicher Tagesverbrauch = Mittelwert aus:
|
||
1. Durchschnittsverbrauch der letzten 12 Monate (pro Tag)
|
||
2. Durchschnittsverbrauch der letzten 3 Monate (pro Tag)
|
||
- Daraus ergibt sich ein gewichteter Tagesverbrauch, der Wachstum beruecksichtigt
|
||
- **Meldebestand** = Tagesverbrauch × konfigurierbare Vorlaufzeit (Default: 14 Tage)
|
||
- **Startphase:** Solange keine ausreichende Produktionshistorie (< 3 Monate) vorhanden ist, wird `ingredients.min_stock_alert` (manueller Wert) als Fallback verwendet
|
||
- **Umschaltung:** Ab Live-Schaltung mit genuegend Daten uebernimmt die Formel automatisch
|
||
- Meldebestand-Quelle: Dynamisch berechnet ODER `ingredients.min_stock_alert` / `packaging_items.min_stock_alert` als Fallback
|
||
|
||
#### 6.2 - Controller
|
||
```bash
|
||
php artisan make:controller Admin/Inventory/StockController --no-interaction
|
||
```
|
||
- Methoden: `ingredients()`, `packaging()`, `labels()`, `shippingOffice()`
|
||
- Jede Methode liefert Bestandsdaten pro Lagerort + Gesamt
|
||
|
||
#### 6.3 - Views
|
||
- `resources/views/admin/inventory/stock/ingredients.blade.php`
|
||
- `resources/views/admin/inventory/stock/packaging.blade.php`
|
||
- `resources/views/admin/inventory/stock/labels.blade.php`
|
||
- `resources/views/admin/inventory/stock/shipping-office.blade.php`
|
||
- Jede View: DataTable mit **dynamischen** Spalten (Lagerorte aus `locations`-Tabelle), Gesamt, Meldebestand, Status
|
||
- Rote Hervorhebung bei Unterschreitung
|
||
- Neue Lagerorte -> automatisch neue Spalten (kein Hardcoding)
|
||
|
||
#### 6.4 - View Composer: Menue-Badges
|
||
- **Datei:** `app/Http/ViewComposers/InventoryBadgeComposer.php`
|
||
- Registrierung in `AppServiceProvider::boot()`:
|
||
```php
|
||
View::composer('layouts.includes.layout-sidenav', InventoryBadgeComposer::class);
|
||
```
|
||
- Zaehlt Artikel unter Meldebestand pro Kategorie
|
||
- Ergebnis wird im Sidenav als rote Badges dargestellt
|
||
- **Performance:** Query mit Caching (5 Minuten) um Seitenlade-Performance nicht zu beeintraechtigen
|
||
|
||
#### 6.5 - Cron-Job: Bestands-Alarm E-Mail
|
||
```bash
|
||
php artisan make:command InventoryAlertCommand --no-interaction
|
||
```
|
||
- Registrierung in `app/Console/Kernel.php`: `->dailyAt('07:00')`
|
||
- Prueft alle Bestaende gegen Meldebestand
|
||
- Sendet zusammenfassende E-Mail an konfigurierbare Adresse (Setting: `inventory_alert_email`)
|
||
|
||
#### 6.6 - E-Mail: Mailable
|
||
```bash
|
||
php artisan make:mail InventoryAlertMail --no-interaction
|
||
```
|
||
- Blade-Template mit Liste der Artikel unter Meldebestand
|
||
|
||
### Tests
|
||
- [ ] Unit-Test: InventoryService Bestandsberechnung (Rohstoffe)
|
||
- [ ] Unit-Test: InventoryService Bestandsberechnung (Packaging via Snapshots)
|
||
- [ ] Unit-Test: InventoryService Threshold-Berechnung
|
||
- [ ] Feature-Test: Bestandsseite zeigt korrekte Werte pro Lagerort
|
||
- [ ] Feature-Test: Dynamische Spalten bei neuem Lagerort
|
||
- [ ] Feature-Test: Badge-Composer liefert korrekte Zahlen
|
||
- [ ] Feature-Test: Cron-Job sendet E-Mail bei Unterschreitung
|
||
|
||
### Deliverable
|
||
Vier Bestandsseiten mit Live-Berechnung, dynamischen Lager-Spalten, Menue-Badges und taeglichem E-Mail-Alarm.
|
||
|
||
---
|
||
|
||
## Phase 7: Ausgang / Ausschuss (2-3 Tage)
|
||
|
||
> **Ziel:** Entsorgungen und Ausschuss erfassen.
|
||
|
||
### Aufgaben
|
||
|
||
#### 7.1 - Model & Migration
|
||
```bash
|
||
php artisan make:model StockDisposal -mf --no-interaction
|
||
```
|
||
- Inkl. `unit` ENUM('gram','piece') Spalte
|
||
|
||
#### 7.2 - Repository
|
||
- `StockDisposalRepository` - CRUD
|
||
|
||
#### 7.3 - Controller & FormRequest
|
||
```bash
|
||
php artisan make:controller Admin/Inventory/StockDisposalController --resource --no-interaction
|
||
```
|
||
|
||
#### 7.4 - Views
|
||
- `index.blade.php` - DataTable mit allen Entsorgungen (filterbar nach Typ + Zeitraum)
|
||
- `create.blade.php` - Formular: Typ, Artikel (dynamisch), Charge (optional), Lagerort, Menge, Grund, Datum
|
||
- `unit` wird automatisch gesetzt je nach Typ
|
||
|
||
#### 7.5 - Integration in Bestandsberechnung
|
||
- `InventoryService` beruecksichtigt `stock_disposals` bereits in der Formel (Phase 6)
|
||
- Sicherstellen, dass Ausgang korrekt abgezogen wird
|
||
|
||
### Tests
|
||
- [ ] Feature-Test: Ausgang erstellen
|
||
- [ ] Feature-Test: Bestand wird nach Ausgang korrekt reduziert
|
||
- [ ] Feature-Test: unit wird korrekt gesetzt
|
||
- [ ] Feature-Test: Grund ist Pflichtfeld
|
||
|
||
### Deliverable
|
||
Vollstaendige Ausschuss-Verwaltung mit Auswirkung auf Bestaende.
|
||
|
||
---
|
||
|
||
## Phase 8: Rollen, Audit-Trail & Feinschliff (3-4 Tage)
|
||
|
||
> **Ziel:** Audit-Trail, Feinschliff Berechtigungen, Code-Qualitaet.
|
||
|
||
### Aufgaben
|
||
|
||
#### 8.1 - Migration: inventory_logs
|
||
```bash
|
||
php artisan make:model InventoryLog -m --no-interaction
|
||
```
|
||
- Polymorphe Struktur: `loggable_type`, `loggable_id`, `action`, `user_id`, `changes` (JSON)
|
||
|
||
#### 8.2 - Observer: Automatisches Logging
|
||
- **Dateien:** `app/Observers/StockEntryObserver.php`, `ProductionObserver.php`, `StockDisposalObserver.php`
|
||
- Registrierung in `AppServiceProvider::boot()`
|
||
- Bei `created`, `updated`: Eintrag in `inventory_logs` mit alten/neuen Werten
|
||
|
||
#### 8.3 - Berechtigungs-Feinschliff
|
||
- Preis-Felder (`price_per_kg`, `price_total`) nur fuer admin >= 7 sichtbar
|
||
- Stammdaten-Bearbeitung nur fuer SuperAdmin
|
||
- Gates oder Policy-Klassen nach Bedarf
|
||
- Ueberpruefung aller Controller auf korrekte Middleware/Autorisierung
|
||
|
||
#### 8.4 - Code-Qualitaet
|
||
- `vendor/bin/pint --dirty` auf alle geaenderten Dateien
|
||
- Alle Feature-Tests durchlaufen lassen
|
||
- Review aller Views auf konsistentes UI (Bootstrap 4)
|
||
|
||
#### 8.5 - Menue-Integration: Finalisierung
|
||
- Alle Menuepunkte mit korrekten Badges
|
||
- Korrekte `active`-Klassen fuer Routing
|
||
- Uebersetzungen in `lang/navigation.php`
|
||
|
||
#### 8.6 - Doku: Inline-Kommentare & PHPDoc
|
||
- PHPDoc-Bloecke fuer alle neuen Services, Repositories, Models
|
||
- Array-Shape-Definitionen fuer komplexe Rueckgabewerte
|
||
|
||
### Tests
|
||
- [ ] Feature-Test: Audit-Log wird bei jeder Bestandsveraenderung erstellt
|
||
- [ ] Feature-Test: CopyReader sieht keine Preise
|
||
- [ ] Feature-Test: SuperAdmin kann alle Stammdaten verwalten
|
||
- [ ] Alle bestehenden Tests laufen weiterhin gruen
|
||
|
||
### Deliverable
|
||
Produktionsreifes System mit Audit-Trail, sauberer Rechtevergabe und vollstaendiger Test-Abdeckung.
|
||
|
||
---
|
||
|
||
## Phase 9: Einstellungen & Konfiguration (1-2 Tage)
|
||
|
||
> **Ziel:** Konfigurierbare Warenwirtschafts-Einstellungen ueber bestehendes Setting-System.
|
||
|
||
### Aufgaben
|
||
|
||
#### 9.1 - Controller
|
||
```bash
|
||
php artisan make:controller Admin/Inventory/InventorySettingController --no-interaction
|
||
```
|
||
- Analog zum bestehenden `SettingController` (`app/Http/Controllers/SettingController.php`)
|
||
- Nutzt `Setting::getContentBySlug()` / `Setting::setContentBySlug()`
|
||
|
||
#### 9.2 - View
|
||
- `resources/views/admin/inventory/settings.blade.php`
|
||
- Felder:
|
||
- E-Mail-Adresse fuer Alarme (Text, Default: `service@gruene-seele.bio`)
|
||
- Alarm aktiv? (Bool, Default: true)
|
||
- Standard-Lagerort (Dropdown aus locations, Default: erster Lagerort)
|
||
|
||
#### 9.3 - Routes
|
||
```php
|
||
Route::middleware(['auth:user', 'superadmin'])->prefix('admin/inventory')->group(function () {
|
||
Route::get('settings', [InventorySettingController::class, 'index'])->name('inventory.settings');
|
||
Route::post('settings', [InventorySettingController::class, 'store'])->name('inventory.settings.store');
|
||
});
|
||
```
|
||
|
||
#### 9.4 - Integration
|
||
- `InventoryAlertCommand` liest E-Mail aus `Setting::getContentBySlug('inventory_alert_email')`
|
||
- `StockEntryController` nutzt `Setting::getContentBySlug('inventory_default_location')` als Default
|
||
|
||
### Tests
|
||
- [ ] Feature-Test: Settings speichern und laden
|
||
- [ ] Feature-Test: Nur SuperAdmin hat Zugriff
|
||
|
||
### Deliverable
|
||
Konfigurierbare Einstellungen fuer die Warenwirtschaft.
|
||
|
||
---
|
||
|
||
## Zusammenfassung: Dateien & Artefakte
|
||
|
||
### Neue Models (13)
|
||
| Model | Tabelle | Phase |
|
||
|-------|---------|-------|
|
||
| Location | locations | 2 |
|
||
| SupplierCategory | supplier_categories | 2 |
|
||
| Supplier | suppliers | 2 |
|
||
| MaterialQuality | material_qualities | 2 |
|
||
| PackagingMaterial | packaging_materials | 2 |
|
||
| PackagingItem | packaging_items | 2 |
|
||
| StockEntry | stock_entries | 3 |
|
||
| ProductPackaging | product_packagings | 4 |
|
||
| Production | productions | 5 |
|
||
| ProductionIngredient | production_ingredients | 5 |
|
||
| ProductionPackaging | production_packagings | 5 |
|
||
| StockDisposal | stock_disposals | 7 |
|
||
| InventoryLog | inventory_logs | 8 |
|
||
|
||
Plus Pivot-Tabelle: `supplier_supplier_category` (kein eigenes Model noetig)
|
||
|
||
### Geaenderte Models (3)
|
||
| Model | Aenderung | Phase |
|
||
|-------|----------|-------|
|
||
| Product | + shelf_life_type, shelf_life_months, Relationen | 1 + 4 |
|
||
| ProductIngredient | + pos, gram, factor | 1 |
|
||
| Ingredient | + default_factor, min_stock_alert | 1 |
|
||
|
||
### Neue Controller (11)
|
||
Alle unter `app/Http/Controllers/Admin/Inventory/`:
|
||
| Controller | Phase |
|
||
|-----------|-------|
|
||
| LocationController | 2 |
|
||
| SupplierCategoryController | 2 |
|
||
| SupplierController | 2 |
|
||
| MaterialQualityController | 2 |
|
||
| PackagingMaterialController | 2 |
|
||
| PackagingItemController | 2 |
|
||
| StockEntryController | 3 |
|
||
| ProductionController | 5 |
|
||
| StockController | 6 |
|
||
| StockDisposalController | 7 |
|
||
| InventorySettingController | 9 |
|
||
|
||
### Neue Repositories (5)
|
||
| Repository | Phase |
|
||
|-----------|-------|
|
||
| SupplierRepository | 2 |
|
||
| PackagingItemRepository | 2 |
|
||
| StockEntryRepository | 3 |
|
||
| ProductionRepository | 5 |
|
||
| StockDisposalRepository | 7 |
|
||
|
||
### Neue Services (2)
|
||
| Service | Phase |
|
||
|---------|-------|
|
||
| `app/Services/ProductionService.php` | 5 |
|
||
| `app/Services/InventoryService.php` | 6 |
|
||
|
||
### Neue Views (~27)
|
||
Alle unter `resources/views/admin/inventory/`:
|
||
- Stammdaten: je `index.blade.php` + `form.blade.php` (12 Views)
|
||
- Eingang: index, create, show (3 Views)
|
||
- Produktion: index, create, show (3 Views)
|
||
- Bestand: 4 Views (je Kategorie)
|
||
- Ausgang: index, create (2 Views)
|
||
- Einstellungen: 1 View
|
||
- Angepasste Views: `admin/product/form.blade.php` (INCI + Packaging)
|
||
|
||
### Neue Artisan Commands (1)
|
||
- `app/Console/Commands/InventoryAlertCommand.php`
|
||
|
||
### Tests (Phase 0 & 1 – umgesetzt)
|
||
- `tests/Feature/ProductPhase0Test.php` – Relationen, Legacy-Sync `product_ingredients`, Dropdown-Sortierung, Attribut-Kopie
|
||
- `tests/Feature/ProductPhase1Test.php` – Pivot Gramm/Faktor/Haltbarkeit, Produktkopie INCI-Pivot
|
||
|
||
### Geaenderte Dateien
|
||
- `routes/web.php` - Neue Route-Gruppen
|
||
- `resources/views/layouts/includes/layout-sidenav.blade.php` - Menuepunkt + Badges
|
||
- `resources/views/admin/product/form.blade.php` - INCI-Tabelle, Warenwirtschaft Haltbarkeit (Phase 1); Packaging spaeter Phase 4
|
||
- `resources/views/admin/product/edit.blade.php` - SortableJS + INCI-JavaScript (Phase 1)
|
||
- `resources/views/admin/ingredient/form.blade.php` - `default_factor`, `min_stock_alert` (Phase 1)
|
||
- `app/Http/Controllers/ProductController.php` - `ingredient_catalog` fuer Produkt-Edit (Phase 1)
|
||
- `app/Http/Controllers/IngredientController.php` - Speichern der neuen Ingredient-Felder (Phase 1)
|
||
- `app/Repositories/ProductRepository.php` - Bugfixes + Erweiterungen
|
||
- `app/Services/HTMLHelper.php` - Sortierung INCI-Dropdown (Phase 0)
|
||
- `app/Providers/AppServiceProvider.php` - View Composer + Observer
|
||
- `app/Console/Kernel.php` - Neuer Cron-Job
|
||
- `app/Models/Product.php` - Relationen + Bugfix
|
||
- `app/Models/Ingredient.php` - Neue Felder + Bugfix (ab Phase 1); Phase 0: nur hasMany-Fix
|
||
- `app/Models/ProductIngredient.php` - Neue Felder (Phase 1)
|
||
- `lang/de/navigation.php` (o.ae.) - Uebersetzungen
|
||
- `database/migrations/2018_09_29_145909_create_countries_table.php` - Phase 0: doppelte Spalte `active` entfernt
|
||
- `database/migrations/2021_06_15_112357_create_product_buys_table.php` - Phase 0: FK `auth_user_id` korrigiert
|
||
- `database/migrations/2026_03_27_111352_add_recipe_fields_to_product_ingredients_table.php` - Phase 1
|
||
- `database/migrations/2026_03_27_111353_add_shelf_life_to_products_table.php` - Phase 1
|
||
- `database/migrations/2026_03_27_111354_add_inventory_fields_to_ingredients_table.php` - Phase 1
|
||
|
||
---
|
||
|
||
## Risiken & Empfehlungen
|
||
|
||
| Risiko | Mitigation |
|
||
|--------|-----------|
|
||
| Performance bei Bestandsberechnung (viele Summen-Queries) | Caching der Badge-Counts (5 Min), InventoryService mit optimierten Aggregat-Queries |
|
||
| Komplexitaet Chargen-Splitting UI | JS-Prototyp frueh bauen, mit Kundin testen |
|
||
| Multi-Lager erweitert sich auf > 2 Standorte | Datenmodell + Views sind bereits dynamisch (n Lagerorte) |
|
||
| BOM-Aenderung nach Produktion | Geloest durch `production_packagings` Snapshot-Tabelle |
|
||
| LUCID-Report nicht im MVP | Als Phase 10 planbar, Datengrundlage ist vorhanden |
|
||
| Bestehende Tests brechen durch Migrations | Tests nutzen SQLite in-memory - Migrations laufen automatisch |
|
||
|
||
---
|
||
|
||
## Empfohlene Reihenfolge fuer sofortigen Nutzen
|
||
|
||
1. **Phase 0 + 1** zuerst - Behebt Bugs und macht die INCI-Verwaltung sofort besser (Quick Wins)
|
||
2. **Phase 2** parallel beginnen - Stammdaten sind unabhaengig
|
||
3. **Phase 3-5** sequentiell - bilden den Kern der Warenwirtschaft
|
||
4. **Phase 6** liefert den groessten Wow-Effekt (Bestandsuebersicht mit Badges)
|
||
5. **Phase 7 + 8 + 9** zum Abschluss - Ausschuss + Audit-Trail + Settings runden ab
|