schema([ Forms\Components\TextInput::make('title') ->label('Name') ->required(), FileUpload::make('images') ->label('Bilder') ->multiple() ->image() ->maxFiles(15) ->directory('product-images') ->panelLayout('grid') ->imagePreviewHeight('200') ->columnSpanFull(), Forms\Components\Textarea::make('description') ->label('Beschreibung'), 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(fn () => [ Forms\Components\TextInput::make('name') ->label('Name') ->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') ->required(), 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') ->label('Verpackung') ->options(function () { return Packing::all()->mapWithKeys(function ($packing) { return [$packing->id => $packing->name . ' (' . $packing->size . ')']; }); }) ->searchable() ->required(), TextInput::make('price') ->label('Preis (in EUR)') ->numeric() ->required(), TextInput::make('sku') ->label('SKU'), Placeholder::make('barcode_preview') ->label('Barcode') ->content(function (Get $get): HtmlString { $gtin = preg_replace('/\D/', '', (string) $get('gtin')); if (strlen($gtin) < 8) { return new HtmlString(''); } $type = match (strlen($gtin)) { 8 => BarcodeGeneratorSVG::TYPE_EAN_8, 13 => BarcodeGeneratorSVG::TYPE_EAN_13, 12 => BarcodeGeneratorSVG::TYPE_UPC_A, 14 => BarcodeGeneratorSVG::TYPE_CODABAR, default => BarcodeGeneratorSVG::TYPE_CODE_128, }; try { $svg = (new BarcodeGeneratorSVG())->getBarcode($gtin, $type, 2, 60); return new HtmlString($svg); } catch (\Throwable) { return new HtmlString(''); } }) ->columnSpanFull() ->visible(fn (Get $get) => strlen(preg_replace('/\D/', '', (string) $get('gtin'))) >= 8), TextInput::make('gtin') ->label('GTIN') ->mask('9 999999 999999') ->dehydrateStateUsing(fn ($state) => preg_replace('/\D/', '', (string) $state)) ->live(debounce: 500), ]) ->label('Größen & Preise') ->columns(2) ->addActionLabel('Option hinzufügen') ->afterStateHydrated(function ($state, $set, $record) { if ($record) { $record->load('sizes'); $sizesData = $record->sizes->map(function ($size) { return [ 'packing_id' => $size->id, 'price' => $size->pivot->price, 'description' => $size->pivot->description, 'sku' => $size->pivot->sku, 'gtin' => $size->pivot->gtin, ]; })->toArray(); $set('sizes', $sizesData); } return $state; }), ]); } public static function table(Table $table): Table { return $table ->columns([ Tables\Columns\ImageColumn::make('images') ->label('Bild') ->disk('public') ->limit(1), Tables\Columns\TextColumn::make('title') ->label('Name') ->sortable() ->searchable(), Tables\Columns\TextColumn::make('recipe.name') ->label('Rezept'), ]); } public static function getRelations(): array { return [ RelationManagers\BatchesRelationManager::class, ]; } public static function getPages(): array { return [ 'index' => Pages\ListProducts::route('/'), 'create' => Pages\CreateProduct::route('/create'), 'edit' => Pages\EditProduct::route('/{record}/edit'), ]; } 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(), ]), ]; } }