From 6fe34883c68b3ef5b38bf309380a67c19bdc6aab Mon Sep 17 00:00:00 2001 From: Malek Tellissi Date: Sat, 12 Sep 2026 17:22:37 +0200 Subject: [PATCH] Add 'Publish to Shopify' for locally-created products New header action on the product edit page creates the entire product (title, description, all variants) in Shopify via API. The returned shopify_product_id and per-variant shopify_variant_ids are stored locally. After success the page reloads so status badges update. Button is hidden once the product is already linked to Shopify. Co-Authored-By: Claude Sonnet 4.6 --- .../ProductResource/Pages/EditProduct.php | 38 +++++++++++++++ app/Services/ShopifyService.php | 48 +++++++++++++++++++ 2 files changed, 86 insertions(+) diff --git a/app/Filament/User/Resources/ProductResource/Pages/EditProduct.php b/app/Filament/User/Resources/ProductResource/Pages/EditProduct.php index 8301292..2c5f469 100644 --- a/app/Filament/User/Resources/ProductResource/Pages/EditProduct.php +++ b/app/Filament/User/Resources/ProductResource/Pages/EditProduct.php @@ -17,6 +17,15 @@ class EditProduct extends EditRecord protected function getHeaderActions(): array { return [ + Actions\Action::make('shopify-create-product') + ->label('In Shopify veröffentlichen') + ->icon('heroicon-o-arrow-up-tray') + ->color('warning') + ->requiresConfirmation() + ->modalHeading('Produkt in Shopify anlegen') + ->modalDescription('Erstellt das Produkt mit allen Varianten in Shopify und verknüpft die IDs.') + ->visible(fn () => ! $this->record->shopify_product_id) + ->action(fn () => $this->createProductInShopify()), Actions\Action::make('rezept-versionen') ->label('Rezept-Versionen') ->icon('heroicon-o-clock') @@ -112,6 +121,35 @@ class EditProduct extends EditRecord $product->update(['recipes_id' => $recipe->id]); } + public function createProductInShopify(): void + { + $product = $this->record; + + if ($product->shopify_product_id) { + Notification::make()->title('Bereits in Shopify verknüpft')->warning()->send(); + return; + } + + $sizes = ProductSize::with('packing')->where('product_id', $product->id)->get(); + + try { + $result = app(ShopifyService::class)->createProduct($product, $sizes); + + $product->update(['shopify_product_id' => $result['shopify_product_id']]); + + foreach ($result['variants'] as $packingId => $variantId) { + ProductSize::where('product_id', $product->id) + ->where('packing_id', $packingId) + ->update(['shopify_variant_id' => $variantId]); + } + + Notification::make()->title('Produkt in Shopify erstellt')->success()->send(); + $this->redirect(ProductResource::getUrl('edit', ['record' => $product])); + } catch (\Throwable $e) { + Notification::make()->title('Shopify-Fehler')->body($e->getMessage())->danger()->send(); + } + } + public function publishVariantToShopify(int $sizeId): void { $size = ProductSize::with('packing')->find($sizeId); diff --git a/app/Services/ShopifyService.php b/app/Services/ShopifyService.php index 7dc9d89..0fe0da7 100644 --- a/app/Services/ShopifyService.php +++ b/app/Services/ShopifyService.php @@ -76,6 +76,54 @@ class ShopifyService return $variants; } + public function createProduct(\App\Models\Product $product, \Illuminate\Support\Collection $sizes): array + { + $variants = $sizes->map(fn (ProductSize $s) => array_filter([ + 'option1' => $s->packing->name . ' ' . $s->packing->size . 'g', + 'price' => number_format((float) $s->price, 2, '.', ''), + 'sku' => $s->sku ?: null, + 'barcode' => $s->gtin ?: null, + ]))->values()->toArray(); + + $body = [ + 'product' => [ + 'title' => $product->title, + 'body_html' => $product->description + ? '

' . nl2br(e($product->description)) . '

' + : '', + ], + ]; + + if (! empty($variants)) { + $body['product']['variants'] = $variants; + } + + $response = Http::withHeader('X-Shopify-Access-Token', $this->token) + ->post("{$this->baseUrl}/products.json", $body); + + if ($response->failed()) { + throw new \RuntimeException( + 'Shopify-Fehler ' . $response->status() . ': ' . $response->body() + ); + } + + $shopifyProduct = $response->json('product'); + $shopifyVariants = $shopifyProduct['variants']; + + // Match returned variants back to local sizes by index (same order as sent) + $variantMap = []; + foreach ($sizes as $index => $size) { + if (isset($shopifyVariants[$index])) { + $variantMap[$size->packing_id] = (string) $shopifyVariants[$index]['id']; + } + } + + return [ + 'shopify_product_id' => (string) $shopifyProduct['id'], + 'variants' => $variantMap, + ]; + } + public function createVariant(string $shopifyProductId, ProductSize $size): string { $packing = $size->packing;