schema([ Section::make('Add-on Details') ->columns(2) ->schema([ TextInput::make('label') ->label('Label') ->required() ->maxLength(255), TextInput::make('key') ->label('Schlüssel') ->required() ->unique(ignoreRecord: true) ->maxLength(191), TextInput::make('variant_id') ->label('Lemon Squeezy Variant-ID') ->helperText('Variant-ID aus Lemon Squeezy für dieses Add-on') ->required(fn (Get $get): bool => (bool) $get('active') && ! is_numeric($get('metadata.price_eur'))) ->maxLength(191), TextInput::make('metadata.price_eur') ->label('PayPal Preis (EUR)') ->helperText('Für PayPal-Checkout erforderlich (z. B. 9.90).') ->numeric() ->step(0.01) ->minValue(0.01) ->required(fn (Get $get): bool => (bool) $get('active') && blank($get('variant_id'))), TextInput::make('sort') ->label('Sortierung') ->numeric() ->default(0), Toggle::make('active') ->label('Aktiv') ->default(true), Placeholder::make('sellable_state') ->label('Verfügbarkeits-Check') ->content(function (Get $get): string { $isActive = (bool) $get('active'); $hasVariant = filled($get('variant_id')); $hasPayPalPrice = is_numeric($get('metadata.price_eur')); if (! $isActive) { return 'Inaktiv'; } if (! $hasVariant && ! $hasPayPalPrice) { return 'Nicht verkäuflich: Variant-ID oder PayPal Preis fehlt.'; } return 'Verkäuflich'; }), ]), Section::make('Limits-Inkremente') ->columns(3) ->schema([ TextInput::make('extra_photos') ->label('Extra Fotos') ->numeric() ->minValue(0) ->default(0), TextInput::make('extra_guests') ->label('Extra Gäste') ->numeric() ->minValue(0) ->default(0), TextInput::make('extra_gallery_days') ->label('Galerie +Tage') ->numeric() ->minValue(0) ->default(0), ]), Section::make('Feature-Entitlements') ->columns(2) ->schema([ Select::make('metadata.scope') ->label('Scope') ->options([ 'photos' => 'Fotos', 'guests' => 'Gäste', 'gallery' => 'Galerie', 'feature' => 'Feature', 'bundle' => 'Bundle', ]) ->native(false) ->searchable(), TagsInput::make('metadata.entitlements.features') ->label('Freigeschaltete Features') ->helperText('Feature-Keys für Freischaltungen, z. B. ai_styling') ->placeholder('z. B. ai_styling') ->columnSpanFull(), DateTimePicker::make('metadata.entitlements.expires_at') ->label('Entitlement gültig bis') ->seconds(false) ->nullable(), ]), ]); } public static function table(Table $table): Table { return $table ->columns([ TextColumn::make('label') ->label('Label') ->searchable() ->sortable(), TextColumn::make('key') ->label('Schlüssel') ->copyable() ->sortable(), TextColumn::make('variant_id') ->label('Lemon Squeezy Variant-ID') ->toggleable() ->copyable(), TextColumn::make('metadata.price_eur') ->label('PayPal Preis (EUR)') ->formatStateUsing(fn (mixed $state): string => is_numeric($state) ? number_format((float) $state, 2, ',', '.').' €' : '—') ->toggleable(), TextColumn::make('metadata.scope') ->label('Scope') ->badge() ->toggleable(), TextColumn::make('metadata.entitlements.features') ->label('Features') ->formatStateUsing(function (mixed $state): string { if (! is_array($state)) { return '—'; } $features = array_values(array_filter(array_map( static fn (mixed $feature): string => trim((string) $feature), $state, ))); return $features === [] ? '—' : implode(', ', $features); }) ->toggleable(), TextColumn::make('extra_photos')->label('Fotos +'), TextColumn::make('extra_guests')->label('Gäste +'), TextColumn::make('extra_gallery_days')->label('Galerietage +'), BadgeColumn::make('active') ->label('Status') ->colors([ 'success' => true, 'danger' => false, ]) ->formatStateUsing(fn (bool $state) => $state ? 'Aktiv' : 'Inaktiv'), BadgeColumn::make('sellability') ->label('Checkout') ->state(fn (PackageAddon $record): string => static::sellabilityLabel($record)) ->colors([ 'success' => fn (string $state): bool => $state === 'Verkäuflich', 'warning' => fn (string $state): bool => $state === 'Unvollständig', 'gray' => fn (string $state): bool => $state === 'Inaktiv', ]), TextColumn::make('sort') ->label('Sort') ->sortable() ->toggleable(isToggledHiddenByDefault: true), ]) ->filters([ Tables\Filters\TernaryFilter::make('active') ->label('Aktiv'), ]) ->actions([ Actions\Action::make('syncLemonSqueezy') ->label('Mit Lemon Squeezy synchronisieren') ->icon('heroicon-o-cloud-arrow-up') ->action(function (PackageAddon $record) { SyncPackageAddonToLemonSqueezy::dispatch($record->id); Notification::make() ->success() ->title('Lemon Squeezy-Sync gestartet') ->body('Das Add-on wird im Hintergrund mit Lemon Squeezy abgeglichen.') ->send(); }), Actions\EditAction::make() ->after(fn (array $data, PackageAddon $record) => app(SuperAdminAuditLogger::class)->recordModelMutation( 'updated', $record, SuperAdminAuditLogger::fieldsMetadata($data), static::class )), ]) ->bulkActions([ Actions\BulkActionGroup::make([ Actions\DeleteBulkAction::make() ->after(function (Collection $records): void { $logger = app(SuperAdminAuditLogger::class); foreach ($records as $record) { $logger->recordModelMutation( 'deleted', $record, source: static::class ); } }), ]), ]); } public static function getPages(): array { return [ 'index' => Pages\ListPackageAddons::route('/'), 'create' => Pages\CreatePackageAddon::route('/create'), 'edit' => Pages\EditPackageAddon::route('/{record}/edit'), ]; } protected static function sellabilityLabel(PackageAddon $record): string { if (! $record->active) { return 'Inaktiv'; } return $record->isSellableForProvider(static::addonProvider()) ? 'Verkäuflich' : 'Unvollständig'; } protected static function addonProvider(): string { return (string) ( config('package-addons.provider') ?? config('checkout.default_provider', CheckoutSession::PROVIDER_PAYPAL) ); } }