columns(1) ->components([ Tabs::make('Style Details') ->tabs([ Tab::make('General') ->components([ Grid::make(2) ->columnSpanFull() ->components([ TextInput::make('title') ->required() ->maxLength(255), Toggle::make('enabled') ->default(true), ]), Grid::make(2) ->columnSpanFull() ->components([ Textarea::make('prompt') ->required() ->rows(5), Textarea::make('description') ->required() ->rows(5), FileUpload::make('preview_source') ->label('Referenzbild für Vorschau') ->helperText('Lade ein Bild hoch, um eine Vorschau mit dem aktuellen Prompt zu generieren.') ->disk('public') ->directory('style_previews/reference') ->image() ->dehydrated(false), ]), Select::make('ai_model_id') ->relationship('aiModel', 'name') ->label(__('filament.resource.style.form.ai_model')) ->required(), FileUpload::make('preview_image') ->disk('public') ->directory('style_previews') ->image() ->imageEditor() ->required(), ]), Tab::make('Details') ->components([ TextInput::make('sort_order') ->label(__('filament.resource.style.form.sort_order')) ->numeric() ->default(0), Textarea::make('parameters') ->label(__('filament.resource.style.form.parameters')) ->nullable() ->rows(15), ]), ]), ]); } public static function table(Table $table): Table { return $table ->defaultSort('sort_order') ->columns([ TextColumn::make('title')->label(__('filament.resource.style.table.title'))->searchable()->sortable(), IconColumn::make('enabled') ->label(__('filament.resource.style.table.enabled')) ->boolean(), TextColumn::make('aiModel.name')->label(__('filament.resource.style.table.ai_model'))->searchable()->sortable(), ImageColumn::make('preview_image')->label(__('filament.resource.style.table.preview_image'))->disk('public'), TextColumn::make('sort_order')->label(__('filament.resource.style.table.sort_order'))->sortable(), ]) ->filters([ SelectFilter::make('ai_model') ->relationship('aiModel', 'name'), ]) ->deferFilters(false) ->headerActions([ Action::make('import_styles') ->label(__('filament.resource.style.import.title')) ->icon('heroicon-o-arrow-up-on-square-stack') ->form([ FileUpload::make('import_file') ->label(__('filament.resource.style.import.file_label')) ->acceptedFileTypes([ 'text/csv', 'text/plain', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', ]) ->directory('imports/styles') ->visibility('private') ->required(), Select::make('ai_model_id') ->label(__('filament.resource.style.import.model_label')) ->relationship('aiModel', 'name') ->required(), ]) ->action(function (array $data): void { $path = storage_path('app/'.$data['import_file']); if (! file_exists($path)) { throw new \RuntimeException('Import-Datei nicht gefunden.'); } $rows = self::parseImportFile($path); if (empty($rows)) { throw new \RuntimeException(__('filament.resource.style.import.empty')); } $created = 0; foreach ($rows as $row) { $title = $row['title'] ?? null; $prompt = $row['prompt'] ?? null; $description = $row['description'] ?? null; if (! $title || ! $prompt) { continue; } Style::create([ 'title' => $title, 'prompt' => $prompt, 'description' => $description ?? '', 'preview_image' => '', // user must set later 'ai_model_id' => $data['ai_model_id'], 'enabled' => true, 'sort_order' => 0, ]); $created++; } if ($created === 0) { throw new \RuntimeException('Es wurden keine Styles importiert.'); } \Filament\Notifications\Notification::make() ->title(__('filament.resource.style.import.title')) ->body($created.' '.__('filament.resource.style.import.success')) ->success() ->send(); }), ]) ->actions([ EditAction::make(), Action::make('duplicate') ->label(__('filament.resource.style.action.duplicate')) ->icon('heroicon-o-document-duplicate') ->action(function (\App\Models\Style $record) { $newStyle = $record->replicate(); $newStyle->title = $record->title.' (Kopie)'; $newStyle->save(); return redirect()->to(\App\Filament\Resources\Styles\StyleResource::getUrl('edit', ['record' => $newStyle->id])); }), ]) ->bulkActions([ BulkActionGroup::make([ DeleteBulkAction::make(), BulkAction::make('enable') ->label(__('filament.resource.style.action.enable_selected')) ->icon('heroicon-o-check-circle') ->action(function (\Illuminate\Support\Collection $records) { $records->each->update(['enabled' => true]); }), BulkAction::make('disable') ->label(__('filament.resource.style.action.disable_selected')) ->icon('heroicon-o-x-circle') ->action(function (\Illuminate\Support\Collection $records) { $records->each->update(['enabled' => false]); }), BulkAction::make('reassignModel') ->label(__('filament.resource.style.action.reassign_model')) ->icon('heroicon-o-arrow-path') ->form([ Select::make('ai_model_id') ->label('Zielmodell') ->relationship('aiModel', 'name') ->required(), ]) ->action(function (\Illuminate\Support\Collection $records, array $data) { $count = 0; foreach ($records as $record) { $record->update(['ai_model_id' => $data['ai_model_id']]); $count++; } \Filament\Notifications\Notification::make() ->title(__('filament.resource.style.action.reassign_model')) ->body("{$count} ".__('filament.resource.style.table.title')) ->success() ->send(); }), BulkAction::make('duplicateToModel') ->label(__('filament.resource.style.action.duplicate_to_model')) ->icon('heroicon-o-document-duplicate') ->form([ Select::make('ai_model_id') ->label('Zielmodell') ->relationship('aiModel', 'name') ->required(), ]) ->action(function (\Illuminate\Support\Collection $records, array $data) { $created = 0; foreach ($records as $record) { $copy = $record->replicate(); $copy->title = $record->title.' (Copy)'; $copy->ai_model_id = $data['ai_model_id']; $copy->save(); $created++; } \Filament\Notifications\Notification::make() ->title(__('filament.resource.style.action.duplicate')) ->body("{$created} Kopien erstellt.") ->success() ->send(); }), ]), ]) ->emptyStateActions([ CreateAction::make(), ]); } public static function getRelations(): array { return [ // ]; } protected static function parseImportFile(string $path): array { return SimpleExcelReader::create($path)->getRows()->map(function (array $row) { $normalized = []; foreach ($row as $key => $value) { $normalized[strtolower(trim($key))] = is_string($value) ? trim($value) : $value; } return [ 'title' => $normalized['title'] ?? $normalized['titel'] ?? null, 'prompt' => $normalized['prompt'] ?? null, 'description' => $normalized['description'] ?? $normalized['beschreibung'] ?? null, ]; })->filter(function (array $row) { return ! empty($row['title']) && ! empty($row['prompt']); })->values()->all(); } public static function getPages(): array { return [ 'index' => Pages\ListStyles::route('/'), 'create' => Pages\CreateStyle::route('/create'), 'edit' => Pages\EditStyle::route('/{record}/edit'), ]; } }