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(), Placeholder::make('description') ->label('Beschreibung') ->content(fn ($record) => $record?->description ?? '—') ->columnSpanFull(), Forms\Components\Section::make('Rezept') ->schema([ Placeholder::make('recipe_version') ->label('Version') ->content(fn ($record) => $record?->recipe ? 'v' . $record->recipe->version . ' · ' . $record->recipe->created_at->format('d.m.Y') : '—'), Repeater::make('recipe_spices') ->label('Zutaten') ->schema([ Select::make('spice_id') ->label('Rohstoff') ->options(Spice::all()->pluck('name', 'id')) ->searchable() ->required(), TextInput::make('amount_in_percent') ->label('Anteil (%)') ->numeric() ->minValue(0.01) ->maxValue(100) ->required(), ]) ->columns(2) ->addActionLabel('Zutat hinzufügen') ->dehydrated(false) ->rules([new SumEquals100()]) ->afterStateHydrated(function ($component, $record) { if ($record?->recipe) { $record->recipe->load('spices'); $component->state( $record->recipe->spices->map(fn ($s) => [ 'spice_id' => $s->id, 'amount_in_percent' => $s->pivot->amount_in_percent, ])->toArray() ); } }), ]) ->columns(2) ->columnSpanFull(), 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'), 'recipe-history' => Pages\RecipeHistory::route('/{record}/rezept-versionen'), ]; } 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(), ]), ]; } }