# SpiceManagementSystem Warenwirtschafts-Panel für Gewürzmischungen — verwaltet Rohstoffe, Rezepte, Produkte, Chargen und Shopify-Sync. Built with [Laravel](https://laravel.com) + [Filament](https://filamentphp.com), deployed via Docker. --- ## Authentifizierung & Rollen Die App verwendet **Nextcloud OAuth 2.0** zur Anmeldung. Es gibt kein lokales Passwort-Login. ### Nextcloud-Setup 1. In Nextcloud unter *Einstellungen → Sicherheit → OAuth 2.0-Clients* einen neuen Client anlegen 2. Weiterleitungs-URI: `https://sms.spices.blue/auth/nextcloud/callback` 3. Client-ID und Secret als Gitea-Secrets hinterlegen (`OAUTH_CLOUDSPICESBLUE_ID`, `OAUTH_CLOUDSPICESBLUE_SECRETKEY`) 4. Redirect-URI als Gitea-Variable hinterlegen (`OAUTH_CLOUDSPICESBLUE_REDIRECTURI`) ### Rollen Rollen werden bei jeder Anmeldung automatisch aus den Nextcloud-Gruppen übernommen (`syncRoles`): | Nextcloud-Gruppe | Rechte | |---|---| | `sms-admin` | Alles: Lesen, Erstellen, Bearbeiten, Löschen, Activity Log | | `sms-manager` | Lesen, Erstellen, Bearbeiten (kein Löschen, kein Activity Log) | | `sms-viewer` | Nur Lesen (alle Einträge öffnen, keine Schreiboperationen) | Benutzer ohne eine dieser Gruppen erhalten einen 403-Fehler. ### Sicherheitshinweise - **Account-Binding per Nextcloud-ID**: Accounts werden primär über die eindeutige Nextcloud-User-ID (`nextcloud_id`) verknüpft, nicht über die E-Mail. Das verhindert Account-Takeover, falls sich Fremde in Nextcloud registrieren können. - **Freie Registrierung deaktivieren**: Auf der Nextcloud-Instanz sollte die Selbstregistrierung neuer Nutzer deaktiviert sein. - **2FA empfohlen**: Für alle Nextcloud-Konten mit Panel-Zugriff sollte Zwei-Faktor-Authentifizierung erzwungen werden — Nextcloud ist der einzige Schlüssel zum Panel. - **CSRF/State**: Laravel Socialite schickt und validiert den `state`-Parameter automatisch. Nie `->stateless()` verwenden. - **Token wird verworfen**: Das Nextcloud-Access-Token wird nach dem Callback nicht gespeichert. Es wird nur verwendet, um Nutzerdaten abzufragen, danach startet eine normale Laravel-Session (HttpOnly-Cookie). --- ## Lokale Entwicklung **Voraussetzungen:** PHP 8.4, Composer, Node.js, MariaDB ```bash composer install npm install && npm run build cp .env.example .env php artisan key:generate php artisan migrate php artisan serve ``` Die App läuft dann auf `http://localhost:8000`. --- ## Produktion (Docker) Der Deploy läuft automatisch via Gitea Actions bei jedem Push auf `main`: 1. Dateien per SCP auf den VPS kopieren 2. `docker compose up -d --build` — baut das Image neu und startet die Container 3. `php artisan migrate --force` — läuft automatisch im Container ### Artisan-Befehle auf dem Server Da die App in Docker mit PHP 8.4 läuft, müssen alle artisan-Befehle **im Container** ausgeführt werden: ```bash cd ~/app && docker compose exec app php artisan ``` --- ## Shopify-Import ### Erstimport (Produkte + Lagerbestand + Beschreibung) ```bash cd ~/app && docker compose exec app php artisan shopify:import ``` Optionen: - `--dry-run` — zeigt nur an was importiert würde, speichert nichts ### Produktbilder herunterladen ```bash cd ~/app && docker compose exec app php artisan shopify:images ``` Optionen: - `--force` — lädt Bilder auch für Produkte neu, die bereits welche haben ### Lagerbestand + Preise synchronisieren ```bash cd ~/app && docker compose exec app php artisan shopify:sync ``` --- ## Rohstofflieferungen Rohstofflieferungen werden unter *Waren Ein/Ausgang → Rohstofflieferungen* erfasst. Neue Rohstoffe können direkt beim Anlegen einer Lieferung erstellt werden. - Mengeneinheit **Gramm**: Eingabe in kg, Speicherung in Gramm - Mengeneinheit **Stück**: optionale Angabe des Gesamtgewichts der Lieferung — daraus wird das **Ø Stückgewicht** als gleitender Durchschnitt über alle Lieferungen berechnet und am Rohstoff gespeichert - Mengeneinheit **Liter**: direkte Eingabe in Litern --- ## Chargen (LOT) & Rohstoff-Abzug Beim Anlegen einer Charge wird der Rohstoffbestand automatisch abgezogen: ``` Charge: 50 Einheiten × 100g Standbodenbeutel = 5000g → Kurkuma (60%): 3000g vom Lager abgezogen → Pfeffer (30%): 1500g vom Lager abgezogen → Ingwer (10%): 500g vom Lager abgezogen ``` - Beim **Bearbeiten** einer Charge wird der Abzug neu berechnet - Beim **Löschen** werden die Mengen zurückgebucht - Chargen können direkt auf der Produktseite angelegt werden ### Chargennummer-Format Jedes Produkt hat einen festen **Chargen-Präfix** (z.B. `112`). Die Chargennummer setzt sich zusammen aus: ``` {lot_prefix}-{JJMMTT} → 112-260912 ``` `JJMMTT` wird beim Anlegen der Charge manuell eingegeben (Jahr/Monat/Tag zweistellig). Das Feld zeigt den Präfix fest an, der Datumsteil ist das einzige Eingabefeld. --- ## Rezept-Versionierung Rezepte sind versioniert und einem Produkt zugeordnet: - Jedes Rezept hat eine Versionsnummer (v1, v2, …) - Beim Anlegen eines neuen Rezepts über das Produktformular wird die Version automatisch hochgezählt - Ältere Versionen bleiben erhalten (z.B. für Chargen-Rückverfolgung) --- ## LOT-API Öffentliche JSON-API zur Rückverfolgung von Chargen. Kein Auth erforderlich. ### Endpunkt ``` GET https://api.sms.spices.blue/api/v1/{lot_number} ``` ### Beispiel-Antwort ```json { "data": { "lot_number": "112-260912", "produced_at": "2024-10-01", "expires_at": "2026-10-01", "units_produced": 50, "quantity_g": 5000, "product": { "name": "Curry Klassik", "description": "..." }, "packing": { "name": "Standbodenbeutel", "size_g": 100, "sku": "CUR-100", "gtin": "4012345678901" }, "recipe": { "version": 3, "created_at": "2024-09-15" }, "spices": [ { "name": "Kurkuma", "percentage": 60.0, "origins": [ { "country": "Indien", "supplier": "Gewürzhandel GmbH", "farmer": "Singh Farm", "notes": null }, { "country": "Sri Lanka", "supplier": "Ceylon Spices", "farmer": null, "notes": "Restmenge aus Vorlieferung" } ] } ] } } ``` Fehler (nicht gefunden): ```json { "error": "LOT-Nummer nicht gefunden." } ``` ### Subdomain-Setup auf dem Server Der Container läuft intern auf `127.0.0.1:5000`. Im Caddy-Reverse-Proxy auf dem Host einfach einen weiteren Block ergänzen — Caddy übernimmt SSL automatisch: ```caddy api.sms.spices.blue { handle /api/* { reverse_proxy 127.0.0.1:5000 } respond 404 } ``` Nur `/api/*`-Pfade werden an den Container weitergeleitet — alle anderen Routen (Panel, Assets usw.) bekommen direkt eine 404 von Caddy, ohne dass Laravel sie zu Gesicht bekommt. Danach `caddy reload` (oder `systemctl reload caddy`). --- ## Cronjob Auf dem Server einrichten, damit der Laravel-Scheduler läuft: ``` * * * * * cd ~/app && docker compose exec -T app php artisan schedule:run >> /dev/null 2>&1 ``` --- ## Datenbankstruktur ``` Table spices { id int [pk] name varchar(255) quantity decimal(10,2) -- immer in Gramm unit varchar } Table recipes { id int [pk] product_id int [ref: > products.id, nullable] version int -- auto-increment pro Produkt name varchar(255) description text } Table recipes_spices { recipe_id int [ref: > recipes.id] spice_id int [ref: > spices.id] amount_in_percent decimal(6,2) } Table packings { id int [pk] name varchar(255) size decimal(10,2) -- Gramm pro Einheit } Table products { id int [pk] title text lot_prefix varchar -- Präfix für Chargennummer, z.B. "112" images json description text recipes_id int [ref: > recipes.id] } Table products_sizes { id int [pk] product_id int [ref: > products.id] packing_id int [ref: > packings.id] sku varchar shopify_variant_id varchar gtin varchar price float } Table products_inventory { id int [pk] product_id int [ref: > products.id] size_id int [ref: > products_sizes.id] out_at timestamp } Table batches { id int [pk] lot_number varchar [unique] product_id int [ref: > products.id] size_id int [ref: > products_sizes.id] recipe_id int [ref: > recipes.id] units_produced int quantity_g decimal(10,2) -- berechnet: units × packing.size produced_at date expires_at date } Table batch_origins { id int [pk] batch_id int [ref: > batches.id] spice_id int [ref: > spices.id] } Table batch_origin_entries { id int [pk] batch_origin_id int [ref: > batch_origins.id] supplier_id int [ref: > suppliers.id, nullable] origin_country varchar farmer_id int [ref: > farmers.id, nullable] notes text } Table farmers { id int [pk] name varchar(255) } Table suppliers { id int [pk] name varchar(255) } ``` --- ## Vision & Todo **Fokus: Warenwirtschaft (Lager, Rohstoffe, Produkte)** - [x] DB-Schema mit Produkthistorie - [x] Rezepte mit Versionierung - [x] Produktgrößen, Preise, SKU, GTIN mit Barcode-Vorschau - [x] Inventar (eindeutige Items mit Ausgangs-Flag) - [x] Chargen (LOT) mit automatischem Rohstoff-Abzug und mehreren Lieferanten/Landwirten pro Rohstoff - [x] Warenausgang (Produktverkauf) - [x] Shopify-Import (Produkte, Varianten, Lagerbestand, Bilder, Beschreibung, Preise) - [x] Continuous Deployment via Gitea Actions + Docker - [x] Frontend (Sidebar, Navigation, Barcode-Preview, Bild-Grid) - [x] LOT-API (JSON, öffentlich, Subdomain api.sms.spices.blue) - [x] Nextcloud OAuth 2.0 Login (kein lokales Passwort) - [x] Rollenbasierte Zugriffskontrolle via Nextcloud-Gruppen (admin / manager / viewer) - [x] Rohstofflieferungen mit Ø Stückgewicht-Kalibrierung - [x] Dashboard mit Rohstoff-Lagerbestand-Chart und Sales-Übersicht