diff --git a/app/Filament/User/Resources/ProductResource.php b/app/Filament/User/Resources/ProductResource.php index 48dd6ea..4120eee 100644 --- a/app/Filament/User/Resources/ProductResource.php +++ b/app/Filament/User/Resources/ProductResource.php @@ -4,26 +4,25 @@ namespace App\Filament\User\Resources; use App\Filament\User\Resources\ProductResource\Pages; use App\Filament\User\Resources\ProductResource\RelationManagers; +use App\Models\Packing; use App\Models\Product; +use App\Models\ProductSize; +use App\Models\Spice; +use App\Rules\SumEquals100; use Filament\Forms; -use Filament\Forms\Form; +use Filament\Forms\Components\Actions\Action as FormAction; use Filament\Forms\Components\FileUpload; +use Filament\Forms\Components\Hidden; use Filament\Forms\Components\Placeholder; use Filament\Forms\Components\Repeater; use Filament\Forms\Components\Select; use Filament\Forms\Components\TextInput; +use Filament\Forms\Form; use Filament\Forms\Get; use Filament\Resources\Resource; use Filament\Tables; use Filament\Tables\Table; use Illuminate\Support\HtmlString; -use App\Models\Packing; -use App\Models\ProductSize; -use App\Models\Recipe; -use App\Models\Spice; -use App\Rules\SumEquals100; -use Filament\Forms\Components\Actions\Action as FormAction; -use Filament\Forms\Components\Hidden; use Picqer\Barcode\BarcodeGeneratorSVG; class ProductResource extends Resource { @@ -235,52 +234,4 @@ class ProductResource extends Resource { public static function getPluralLabel(): string { return 'Produkte'; } - - - protected function mutateFormDataBeforeCreate(array $data): array { - if (isset($data['images'])) { - $data['images'] = json_encode($data['images']); - } - return $data; - } - - protected function mutateFormDataBeforeUpdate(array $data): array { - if (isset($data['images'])) { - $data['images'] = json_encode($data['images']); - } - return $data; - } - - public static function getRecord($id) { - return Product::with('sizes')->findOrFail($id); // Eager load spices relationship - } - - public static function getTableRecordTitle() { - return function ($record) { - $images = json_decode($record->images, true); - return implode(', ', $images); - }; - } - - public static function getActions(): array { - return [ - Actions\DeleteAction::make(), - Action::make('deleteImage') - ->action(function ($record, $data) { - $images = json_decode($record->images, true); - $index = array_search($data['image'], $images); - if ($index !== false) { - unset($images[$index]); - $record->update(['images' => array_values($images)]); - } - }) - ->form([ - Forms\Components\Select::make('image') - ->options(function ($record) { - return array_combine($record->images, $record->images); - }) - ->required(), - ]), - ]; - } } diff --git a/app/Filament/User/Resources/ProductResource/Pages/EditProduct.php b/app/Filament/User/Resources/ProductResource/Pages/EditProduct.php index cf79ac7..ffb3337 100644 --- a/app/Filament/User/Resources/ProductResource/Pages/EditProduct.php +++ b/app/Filament/User/Resources/ProductResource/Pages/EditProduct.php @@ -31,6 +31,15 @@ class EditProduct extends EditRecord ->exists() ) ->action(fn () => $this->createProductInShopify()), + Actions\Action::make('pull-from-shopify') + ->label('Aus Shopify laden') + ->icon('heroicon-o-arrow-down-tray') + ->color('gray') + ->requiresConfirmation() + ->modalHeading('Aus Shopify aktualisieren') + ->modalDescription('Überschreibt Beschreibung und Variantenpreise/-SKUs mit den aktuellen Shopify-Daten.') + ->visible(fn () => (bool) $this->record->shopify_product_id) + ->action(fn () => $this->pullFromShopify()), Actions\Action::make('rezept-versionen') ->label('Rezept-Versionen') ->icon('heroicon-o-clock') @@ -181,6 +190,50 @@ class EditProduct extends EditRecord } } + public function pullFromShopify(): void + { + $product = $this->record; + + if (! $product->shopify_product_id) { + Notification::make()->title('Kein Shopify-Produkt verknüpft')->warning()->send(); + return; + } + + try { + $data = app(ShopifyService::class)->getProduct($product->shopify_product_id); + + $updates = []; + if (! empty($data['description'])) { + $updates['description'] = $data['description']; + } + if ($updates) { + $product->update($updates); + } + + $variantMap = collect($data['variants'])->keyBy('shopify_variant_id'); + + ProductSize::where('product_id', $product->id) + ->whereNotNull('shopify_variant_id') + ->get() + ->each(function (ProductSize $size) use ($variantMap) { + $shopifyVariant = $variantMap->get($size->shopify_variant_id); + if (! $shopifyVariant) { + return; + } + $size->update([ + 'price' => $shopifyVariant['price'], + 'sku' => $shopifyVariant['sku'] ?: $size->sku, + 'gtin' => $shopifyVariant['gtin'] ?: $size->gtin, + ]); + }); + + Notification::make()->title('Aus Shopify aktualisiert')->success()->send(); + $this->redirect(ProductResource::getUrl('edit', ['record' => $product])); + } catch (\Throwable $e) { + Notification::make()->title('Shopify-Fehler')->body($e->getMessage())->danger()->send(); + } + } + public function syncVariantWithShopify(int $sizeId): void { $size = ProductSize::find($sizeId); diff --git a/app/Services/ShopifyService.php b/app/Services/ShopifyService.php index 0fe0da7..58aba8d 100644 --- a/app/Services/ShopifyService.php +++ b/app/Services/ShopifyService.php @@ -2,6 +2,7 @@ namespace App\Services; +use App\Models\Product; use App\Models\ProductSize; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; @@ -76,14 +77,38 @@ class ShopifyService return $variants; } - public function createProduct(\App\Models\Product $product, \Illuminate\Support\Collection $sizes): array + public function getProduct(string $shopifyProductId): array + { + $response = Http::withHeader('X-Shopify-Access-Token', $this->token) + ->get("{$this->baseUrl}/products/{$shopifyProductId}.json"); + + if ($response->failed()) { + throw new \RuntimeException( + 'Shopify-Fehler ' . $response->status() . ': ' . $response->body() + ); + } + + $p = $response->json('product'); + + return [ + 'description' => strip_tags($p['body_html'] ?? ''), + 'variants' => collect($p['variants'] ?? [])->map(fn ($v) => [ + 'shopify_variant_id' => (string) $v['id'], + 'price' => (float) ($v['price'] ?? 0), + 'sku' => $v['sku'] ?? null, + 'gtin' => $v['barcode'] ?? null, + ])->all(), + ]; + } + + public function createProduct(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(); + ], fn ($v) => $v !== null))->values()->toArray(); $body = [ 'product' => [ @@ -91,6 +116,7 @@ class ShopifyService 'body_html' => $product->description ? '

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

' : '', + 'options' => [['name' => 'Größe']], ], ]; @@ -98,9 +124,20 @@ class ShopifyService $body['product']['variants'] = $variants; } + Log::info('Shopify createProduct request', [ + 'product_id' => $product->id, + 'sizes' => $sizes->count(), + 'body' => $body, + ]); + $response = Http::withHeader('X-Shopify-Access-Token', $this->token) ->post("{$this->baseUrl}/products.json", $body); + Log::info('Shopify createProduct response', [ + 'status' => $response->status(), + 'body' => $response->json(), + ]); + if ($response->failed()) { throw new \RuntimeException( 'Shopify-Fehler ' . $response->status() . ': ' . $response->body()