From a7f5c4a83e343fb957b98eadecd9c4b6cb9e62f3 Mon Sep 17 00:00:00 2001 From: Malek Tellissi Date: Sat, 12 Sep 2026 05:18:11 +0200 Subject: [PATCH] Recipe versioning, inline recipe create, recipe summary, Shopify description - 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 --- app/Console/Commands/ShopifyImport.php | 4 ++- .../User/Resources/ProductResource.php | 34 ++++++++++++++++--- app/Models/Product.php | 5 +++ app/Models/Recipe.php | 7 +++- app/Services/ShopifyService.php | 9 ++--- ..._add_version_and_product_id_to_recipes.php | 25 ++++++++++++++ 6 files changed, 74 insertions(+), 10 deletions(-) create mode 100644 database/migrations/2026_09_12_031510_add_version_and_product_id_to_recipes.php diff --git a/app/Console/Commands/ShopifyImport.php b/app/Console/Commands/ShopifyImport.php index ce15150..1288162 100644 --- a/app/Console/Commands/ShopifyImport.php +++ b/app/Console/Commands/ShopifyImport.php @@ -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( diff --git a/app/Filament/User/Resources/ProductResource.php b/app/Filament/User/Resources/ProductResource.php index 785f9bd..7e110b6 100644 --- a/app/Filament/User/Resources/ProductResource.php +++ b/app/Filament/User/Resources/ProductResource.php @@ -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('Keine Zutaten hinterlegt'); + } + $items = $recipe->spices->map(fn ($s) => "
  • {$s->name} ({$s->pivot->amount_in_percent}%)
  • ")->implode(''); + $meta = "v{$recipe->version} ยท erstellt " . $recipe->created_at->format('d.m.Y'); + return new HtmlString("

    {$meta}

    "); + }) + ->columnSpanFull() + ->visible(fn (Forms\Get $get, $record) => filled($get('recipes_id')) || $record?->recipes_id), Repeater::make('sizes') ->schema([ Select::make('packing_id') diff --git a/app/Models/Product.php b/app/Models/Product.php index f07bfe5..b9740c6 100644 --- a/app/Models/Product.php +++ b/app/Models/Product.php @@ -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') diff --git a/app/Models/Recipe.php b/app/Models/Recipe.php index caf1514..a244ad5 100644 --- a/app/Models/Recipe.php +++ b/app/Models/Recipe.php @@ -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() { diff --git a/app/Services/ShopifyService.php b/app/Services/ShopifyService.php index b57734d..9bc2dc6 100644 --- a/app/Services/ShopifyService.php +++ b/app/Services/ShopifyService.php @@ -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, ]; } } diff --git a/database/migrations/2026_09_12_031510_add_version_and_product_id_to_recipes.php b/database/migrations/2026_09_12_031510_add_version_and_product_id_to_recipes.php new file mode 100644 index 0000000..eb21393 --- /dev/null +++ b/database/migrations/2026_09_12_031510_add_version_and_product_id_to_recipes.php @@ -0,0 +1,25 @@ +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']); + }); + } +};