bootedInteractsWithTable() calls table() before mount() sets $this->record. Accessing $this->record eagerly causes a TypeError on the uninitialized typed property, which Filament/Livewire converts to a 404. Deferred all $this->record accesses into closures so they only execute at render time (after mount() has run). 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.
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
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
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": "LOT-2024-001",
"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,
"origin": {
"country": "Indien",
"farmer": "Singh Farm",
"supplier": "Gewürzhandel GmbH",
"notes": null
}
}
]
}
}
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(4,2)
}
Table packings {
id int [pk]
name varchar(255)
size decimal(10,2) -- Gramm pro Einheit
}
Table products {
id int [pk]
title text
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]
supplier_id int [ref: > suppliers.id, nullable]
origin_country varchar
farmer varchar
notes text
}
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
- 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)
- Dashboard (Füllstände, Lager, Sales-Zeitspanne)