Deploy / deploy (push) Successful in 10s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
311 lines
7.2 KiB
Markdown
311 lines
7.2 KiB
Markdown
# 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.
|
||
|
||
---
|
||
|
||
## 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 <befehl>
|
||
```
|
||
|
||
---
|
||
|
||
## 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
|
||
```
|
||
|
||
---
|
||
|
||
## 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)
|
||
- [ ] Dashboard (Füllstände, Lager, Sales-Zeitspanne)
|