Malek TellissiandClaude Sonnet 4.6 fc4579c47d
Deploy / deploy (push) Successful in 28s
Add Nextcloud OAuth 2.0 login via laravel/socialite
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-13 01:40:46 +02:00
2026-09-11 01:17:47 +02:00
2026-09-11 01:17:47 +02:00
2026-09-11 01:17:47 +02:00
2026-09-11 01:17:47 +02:00
2026-09-11 01:17:47 +02:00
2026-09-11 01:17:47 +02:00
2026-09-11 01:17:47 +02:00
2026-09-11 01:17:47 +02:00
2026-09-11 01:17:47 +02:00
2026-09-11 01:17:47 +02:00
2026-09-11 01:17:47 +02:00
2026-09-11 01:17:47 +02:00
2026-09-11 01:17:47 +02:00
2026-09-11 01:17:47 +02:00
2026-09-11 01:17:47 +02:00
2026-09-11 01:17:47 +02:00

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:

  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:

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

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)
  • Dashboard (Füllstände, Lager, Sales-Zeitspanne)
S
Description
No description provided
Readme
1.3 MiB
0 Stars 1 Watchers 0 Forks
Languages
PHP 97.5%
Blade 1.4%
Dockerfile 0.6%
Shell 0.4%
JavaScript 0.1%