Recipe versioning, inline recipe create, recipe summary, Shopify description
Deploy / deploy (push) Successful in 15s

- Add version + product_id to recipes table (migration)
- Recipe select shows version label (e.g. "Kurkuma v2")
- Creating recipe from product page pre-fills product name, auto-increments version
- Recipe ingredient summary shown read-only below the select
- Remove package_content field (redundant with recipe ingredients)
- Pull product description from Shopify body_html on import
- Update existing products with empty description on re-import

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Malek TellissiandClaude Sonnet 4.6 committed 2026-09-12 05:18:11 +02:00
1 parent 6e28984278
commit a7f5c4a83e
6 files changed
+74 -10

No files matched your search

+3 -1
View File
@@ -58,7 +58,7 @@ class ShopifyImport extends Command
$product = Product::firstOrCreate(
['title' => $variant['product_title']],
['recipes_id' => null],
['recipes_id' => null, 'description' => $variant['product_description']],
);
if ($product->wasRecentlyCreated) {
$stats['products']++;
@@ -67,6 +67,8 @@ class ShopifyImport extends Command
'product' => $product,
'image_urls' => $variant['image_urls'],
];
} elseif (empty($product->description) && ! empty($variant['product_description'])) {
$product->update(['description' => $variant['product_description']]);
}
$size = ProductSize::firstOrCreate(
@@ -18,6 +18,7 @@ use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Support\HtmlString;
use App\Models\Packing;
use App\Models\Recipe;
use Picqer\Barcode\BarcodeGeneratorSVG;
class ProductResource extends Resource {
@@ -45,21 +46,32 @@ class ProductResource extends Resource {
->columnSpanFull(),
Forms\Components\Textarea::make('description')
->label('Beschreibung'),
Forms\Components\Textarea::make('package_content')
->label('Inhaltsangabe'),
Forms\Components\Select::make('recipes_id')
->label('Rezept')
->native(false)
->searchable()
->live()
->relationship('recipe', 'name')
->getOptionLabelFromRecordUsing(fn (Recipe $r) => "{$r->name} (v{$r->version})")
->required()
->createOptionForm([
->createOptionForm(fn () => [
Forms\Components\TextInput::make('name')
->label('Name')
->required(),
->required()
->default(fn ($livewire) => $livewire->data['title'] ?? ''),
Forms\Components\Textarea::make('description')
->label('Beschreibung'),
])
->createOptionUsing(function (array $data, $livewire) {
$productId = $livewire->record?->id;
$nextVersion = Recipe::where('product_id', $productId)->max('version') + 1;
return Recipe::create([
'name' => $data['name'],
'description' => $data['description'] ?? null,
'product_id' => $productId,
'version' => $nextVersion,
])->getKey();
})
->editOptionForm([
Forms\Components\TextInput::make('name')
->label('Name')
@@ -67,6 +79,20 @@ class ProductResource extends Resource {
Forms\Components\Textarea::make('description')
->label('Beschreibung'),
]),
Placeholder::make('recipe_summary')
->label('Rezept-Inhalt')
->content(function (Forms\Get $get, $record): HtmlString {
$recipeId = $get('recipes_id');
$recipe = $recipeId ? Recipe::with('spices')->find($recipeId) : $record?->recipe?->load('spices');
if (! $recipe || $recipe->spices->isEmpty()) {
return new HtmlString('<span class="text-sm text-gray-400">Keine Zutaten hinterlegt</span>');
}
$items = $recipe->spices->map(fn ($s) => "<li>{$s->name} ({$s->pivot->amount_in_percent}%)</li>")->implode('');
$meta = "v{$recipe->version} · erstellt " . $recipe->created_at->format('d.m.Y');
return new HtmlString("<p class='text-xs text-gray-400 mb-1'>{$meta}</p><ul class='list-disc list-inside text-sm'>{$items}</ul>");
})
->columnSpanFull()
->visible(fn (Forms\Get $get, $record) => filled($get('recipes_id')) || $record?->recipes_id),
Repeater::make('sizes')
->schema([
Select::make('packing_id')
+5
View File
@@ -19,6 +19,11 @@ class Product extends Model
return $this->belongsTo(Recipe::class, 'recipes_id');
}
public function recipeVersions()
{
return $this->hasMany(Recipe::class)->orderByDesc('version');
}
public function sizes()
{
return $this->belongsToMany(Packing::class, 'products_sizes')
+6 -1
View File
@@ -9,7 +9,12 @@ class Recipe extends Model
{
use HasFactory;
protected $fillable = ['name', 'description'];
protected $fillable = ['name', 'description', 'product_id', 'version'];
public function product()
{
return $this->belongsTo(Product::class);
}
public function spices()
{
+5 -4
View File
@@ -25,7 +25,7 @@ class ShopifyService
public function getAllVariants(): array
{
$variants = [];
$url = "{$this->baseUrl}/products.json?limit=250&fields=id,title,variants,images";
$url = "{$this->baseUrl}/products.json?limit=250&fields=id,title,body_html,variants,images";
while ($url) {
$response = Http::withHeader('X-Shopify-Access-Token', $this->token)->get($url);
@@ -55,9 +55,10 @@ class ShopifyService
'sku' => $variant['sku'] ?? '',
'price' => (float) ($variant['price'] ?? 0),
'inventory_quantity' => (int) ($variant['inventory_quantity'] ?? 0),
'product_title' => $product['title'],
'option1' => $variant['option1'] ?? '',
'image_urls' => $imageUrls,
'product_title' => $product['title'],
'product_description' => strip_tags($product['body_html'] ?? ''),
'option1' => $variant['option1'] ?? '',
'image_urls' => $imageUrls,
];
}
}
@@ -0,0 +1,25 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('recipes', function (Blueprint $table) {
$table->unsignedBigInteger('product_id')->nullable()->after('id');
$table->unsignedInteger('version')->default(1)->after('product_id');
$table->foreign('product_id')->references('id')->on('products')->nullOnDelete();
});
}
public function down(): void
{
Schema::table('recipes', function (Blueprint $table) {
$table->dropForeign(['product_id']);
$table->dropColumn(['product_id', 'version']);
});
}
};