Filament checks view before edit for row URL. Added a shared ViewRecord base that redirects to edit if canEdit() is true, so admins and managers get the editable form directly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SpiceManagementSystem
Warenwirtschafts-Panel für Gewürzmischungen — verwaltet Rohstoffe, Rezepte, Produkte, Chargen und Shopify-Sync.
Built with Laravel + Filament, deployed via Docker.
Authentifizierung & Rollen
Die App verwendet Nextcloud OAuth 2.0 zur Anmeldung. Es gibt kein lokales Passwort-Login.
Nextcloud-Setup
- In Nextcloud unter Einstellungen → Sicherheit → OAuth 2.0-Clients einen neuen Client anlegen
- Weiterleitungs-URI:
https://sms.spices.blue/auth/nextcloud/callback - Client-ID und Secret als Gitea-Secrets hinterlegen (
OAUTH_CLOUDSPICESBLUE_ID,OAUTH_CLOUDSPICESBLUE_SECRETKEY) - 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
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:
- Dateien per SCP auf den VPS kopieren
docker compose up -d --build— baut das Image neu und startet die Containerphp 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:
cd ~/app && docker compose exec app php artisan <befehl>
Shopify-Import
Erstimport (Produkte + Lagerbestand + Beschreibung)
cd ~/app && docker compose exec app php artisan shopify:import
Optionen:
--dry-run— zeigt nur an was importiert würde, speichert nichts
Produktbilder herunterladen
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
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
{
"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):
{ "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:
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)
- DB-Schema mit Produkthistorie
- Rezepte mit Versionierung
- Produktgrößen, Preise, SKU, GTIN mit Barcode-Vorschau
- Inventar (eindeutige Items mit Ausgangs-Flag)
- Chargen (LOT) mit automatischem Rohstoff-Abzug und mehreren Lieferanten/Landwirten pro Rohstoff
- Warenausgang (Produktverkauf)
- Shopify-Import (Produkte, Varianten, Lagerbestand, Bilder, Beschreibung, Preise)
- Continuous Deployment via Gitea Actions + Docker
- Frontend (Sidebar, Navigation, Barcode-Preview, Bild-Grid)
- LOT-API (JSON, öffentlich, Subdomain api.sms.spices.blue)
- Nextcloud OAuth 2.0 Login (kein lokales Passwort)
- Rollenbasierte Zugriffskontrolle via Nextcloud-Gruppen (admin / manager / viewer)
- Rohstofflieferungen mit Ø Stückgewicht-Kalibrierung
- Dashboard mit Rohstoff-Lagerbestand-Chart und Sales-Übersicht