Deploy / deploy (push) Successful in 14s
- Fix root cause: recipes_spices.amount_in_percent was decimal(4,2) which overflows on 100% — widened to decimal(6,2) via migration - Wrap afterSave() in try-catch so DB/logic errors show as a persistent danger notification instead of a blank 500 page - Same for CreateProduct: errors show as notification; also reads recipe_spices from raw Livewire $this->data (not $data param which excludes dehydrated(false) fields) so recipes actually save on create Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
263 lines
9.3 KiB
PHP
263 lines
9.3 KiB
PHP
<?php
|
|
|
|
namespace App\Filament\User\Resources\ProductResource\Pages;
|
|
|
|
use App\Filament\User\Resources\ProductResource;
|
|
use App\Models\ProductSize;
|
|
use App\Models\Recipe;
|
|
use App\Services\ShopifyService;
|
|
use Filament\Actions;
|
|
use Filament\Notifications\Notification;
|
|
use Filament\Resources\Pages\EditRecord;
|
|
|
|
class EditProduct extends EditRecord
|
|
{
|
|
protected static string $resource = ProductResource::class;
|
|
|
|
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 &&
|
|
! ProductSize::where('product_id', $this->record->id)
|
|
->whereNotNull('shopify_variant_id')
|
|
->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')
|
|
->color('gray')
|
|
->url(fn () => ProductResource::getUrl('recipe-history', ['record' => $this->record])),
|
|
Actions\DeleteAction::make(),
|
|
];
|
|
}
|
|
|
|
protected function afterSave(): void
|
|
{
|
|
try {
|
|
$this->syncSizes();
|
|
$this->syncRecipe();
|
|
} catch (\Throwable $e) {
|
|
Notification::make()
|
|
->title('Fehler beim Speichern')
|
|
->body($e->getMessage())
|
|
->danger()
|
|
->persistent()
|
|
->send();
|
|
}
|
|
}
|
|
|
|
protected function syncSizes(): void
|
|
{
|
|
$product = $this->record;
|
|
$incoming = collect($this->data['sizes'] ?? []);
|
|
$keepIds = [];
|
|
|
|
foreach ($incoming as $size) {
|
|
if (empty($size['packing_id'])) {
|
|
continue;
|
|
}
|
|
|
|
$gtin = preg_replace('/\D/', '', (string) ($size['gtin'] ?? ''));
|
|
|
|
$ps = ProductSize::updateOrCreate(
|
|
[
|
|
'product_id' => $product->id,
|
|
'packing_id' => (int) $size['packing_id'],
|
|
],
|
|
[
|
|
'price' => $size['price'],
|
|
'sku' => $size['sku'] ?: null,
|
|
'gtin' => $gtin !== '' ? $gtin : null,
|
|
'description' => $size['description'] ?? null,
|
|
]
|
|
);
|
|
|
|
$keepIds[] = $ps->id;
|
|
}
|
|
|
|
// Only delete local-only sizes (no shopify_variant_id) that were removed
|
|
ProductSize::where('product_id', $product->id)
|
|
->whereNotIn('id', $keepIds)
|
|
->whereNull('shopify_variant_id')
|
|
->delete();
|
|
}
|
|
|
|
protected function syncRecipe(): void
|
|
{
|
|
$product = $this->record;
|
|
$spices = $this->data['recipe_spices'] ?? [];
|
|
|
|
if (empty($spices)) {
|
|
return;
|
|
}
|
|
|
|
$newPivot = collect($spices)->mapWithKeys(fn ($s) => [
|
|
(int) $s['spice_id'] => (float) $s['amount_in_percent'],
|
|
])->toArray();
|
|
|
|
$current = $product->recipe;
|
|
|
|
if ($current) {
|
|
$current->load('spices');
|
|
|
|
$existingPivot = $current->spices->mapWithKeys(fn ($s) => [
|
|
$s->id => (float) $s->pivot->amount_in_percent,
|
|
])->toArray();
|
|
|
|
if ($existingPivot == $newPivot) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
$nextVersion = Recipe::where('product_id', $product->id)->max('version') + 1;
|
|
|
|
$recipe = Recipe::create([
|
|
'name' => $product->title,
|
|
'product_id' => $product->id,
|
|
'version' => $nextVersion,
|
|
]);
|
|
|
|
$recipe->spices()->sync(
|
|
collect($newPivot)->mapWithKeys(fn ($pct, $id) => [
|
|
$id => ['amount_in_percent' => $pct],
|
|
])->toArray()
|
|
);
|
|
|
|
$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);
|
|
$product = $this->record;
|
|
|
|
if (! $size || $size->shopify_variant_id || ! $product->shopify_product_id) {
|
|
Notification::make()
|
|
->title('Nicht möglich')
|
|
->body($product->shopify_product_id
|
|
? 'Variante bereits in Shopify verknüpft.'
|
|
: 'Produkt hat keine Shopify-ID — zuerst shopify:import ausführen.')
|
|
->warning()
|
|
->send();
|
|
return;
|
|
}
|
|
|
|
try {
|
|
$variantId = app(ShopifyService::class)->createVariant($product->shopify_product_id, $size);
|
|
$size->update(['shopify_variant_id' => $variantId]);
|
|
Notification::make()->title('Variante in Shopify veröffentlicht')->success()->send();
|
|
$this->redirect(ProductResource::getUrl('edit', ['record' => $product]));
|
|
} catch (\Throwable $e) {
|
|
Notification::make()->title('Shopify-Fehler')->body($e->getMessage())->danger()->send();
|
|
}
|
|
}
|
|
|
|
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);
|
|
|
|
if (! $size || ! $size->shopify_variant_id) {
|
|
Notification::make()->title('Keine Shopify-Verknüpfung')->warning()->send();
|
|
return;
|
|
}
|
|
|
|
try {
|
|
app(ShopifyService::class)->updateVariant($size);
|
|
Notification::make()->title('Shopify synchronisiert')->success()->send();
|
|
} catch (\Throwable $e) {
|
|
Notification::make()->title('Shopify-Fehler')->body($e->getMessage())->danger()->send();
|
|
}
|
|
}
|
|
}
|