schema([ TextInput::make('user.full_name') ->label(__('admin.tenants.fields.name')) ->required() ->readOnly() ->dehydrated(false) ->default(fn (Tenant $record) => $record->user?->full_name ?? 'Unbekannt'), TextInput::make('slug') ->label(__('admin.tenants.fields.slug')) ->required() ->unique(ignoreRecord: true) ->maxLength(255), TextInput::make('contact_email') ->label(__('admin.tenants.fields.contact_email')) ->email() ->required() ->maxLength(255), TextInput::make('paddle_customer_id') ->label('Paddle Customer ID') ->maxLength(191) ->helperText('Verknuepfung mit Paddle Billing Kundenkonto.') ->nullable(), TextInput::make('total_revenue') ->label(__('admin.tenants.fields.total_revenue')) ->prefix('€') ->numeric() ->step(0.01) ->readOnly(), Select::make('active_reseller_package_id') ->label(__('admin.tenants.fields.active_reseller_package')) ->relationship('activeResellerPackage.package', 'name') ->searchable() ->preload() ->nullable(), TextInput::make('remaining_events') ->label(__('admin.tenants.fields.remaining_events')) ->readOnly() ->dehydrated(false) ->default(fn (Tenant $record) => $record->activeResellerPackage?->remaining_events ?? 0), Toggle::make('is_active') ->label(__('admin.tenants.fields.is_active')) ->default(true), Toggle::make('is_suspended') ->label(__('admin.tenants.fields.is_suspended')) ->default(false), KeyValue::make('features') ->label(__('admin.tenants.fields.features')) ->keyLabel(__('admin.common.key')) ->valueLabel(__('admin.common.value')), ])->columns(2); } public static function infolist(Schema $schema): Schema { return TenantInfolist::configure($schema); } public static function getRecordSubNavigation(Page $page): array { return $page->generateNavigationItems([ Pages\ViewTenant::class, Pages\ViewTenantLifecycle::class, Pages\EditTenant::class, ]); } public static function table(Table $table): Table { return $table ->columns([ Tables\Columns\TextColumn::make('id')->sortable(), Tables\Columns\TextColumn::make('user.full_name') ->label(__('admin.tenants.fields.name')) ->searchable() ->sortable() ->getStateUsing(fn (Tenant $record) => $record->user?->full_name ?? 'Unbekannt'), Tables\Columns\TextColumn::make('slug')->searchable(), Tables\Columns\TextColumn::make('contact_email'), Tables\Columns\TextColumn::make('paddle_customer_id') ->label('Paddle Customer') ->toggleable(isToggledHiddenByDefault: true) ->formatStateUsing(fn ($state) => $state ?: '-'), Tables\Columns\TextColumn::make('active_reseller_package_id') ->label(__('admin.tenants.fields.active_package')) ->badge() ->color('success') ->getStateUsing(fn (Tenant $record) => $record->activeResellerPackage?->name ?? 'Kein aktives Package'), Tables\Columns\TextColumn::make('active_reseller_package_id') ->label(__('admin.tenants.fields.remaining_events')) ->badge() ->color(fn ($state) => $state < 1 ? 'danger' : 'success') ->getStateUsing(fn (Tenant $record) => $record->activeResellerPackage?->remaining_events ?? 0), Tables\Columns\TextColumn::make('active_reseller_package_id') ->dateTime() ->label(__('admin.tenants.fields.package_expires_at')) ->badge() ->color(fn ($state) => $state && $state->isPast() ? 'danger' : 'success') ->getStateUsing(fn (Tenant $record) => $record->activeResellerPackage?->expires_at), Tables\Columns\TextColumn::make('total_revenue') ->money('EUR') ->sortable(), Tables\Columns\IconColumn::make('is_active') ->boolean() ->color(fn (bool $state): string => $state ? 'success' : 'danger'), Tables\Columns\IconColumn::make('is_suspended') ->label(__('admin.tenants.fields.is_suspended')) ->boolean() ->color(fn (bool $state): string => $state ? 'warning' : 'success') ->toggleable(isToggledHiddenByDefault: true), Tables\Columns\TextColumn::make('pending_deletion_at') ->label(__('admin.tenants.fields.pending_deletion_at')) ->dateTime() ->toggleable(isToggledHiddenByDefault: true), Tables\Columns\TextColumn::make('deletion_warning_sent_at') ->label(__('admin.tenants.fields.deletion_warning_sent_at')) ->dateTime() ->toggleable(isToggledHiddenByDefault: true), Tables\Columns\TextColumn::make('anonymized_at') ->label(__('admin.tenants.fields.anonymized_at')) ->dateTime() ->toggleable(isToggledHiddenByDefault: true), Tables\Columns\TextColumn::make('last_activity_at')->since()->label(__('admin.common.last_activity')), Tables\Columns\TextColumn::make('created_at')->dateTime()->toggleable(isToggledHiddenByDefault: true), ]) ->filters([]) ->actions([ Actions\EditAction::make(), Actions\Action::make('add_package') ->label('Package hinzufügen') ->icon('heroicon-o-plus') ->form([ Select::make('package_id') ->label('Package') ->options(\App\Models\Package::where('type', 'reseller')->pluck('name', 'id')) ->searchable() ->preload() ->required(), Forms\Components\DateTimePicker::make('expires_at') ->label('Ablaufdatum') ->default(now()->addYear()), Forms\Components\Textarea::make('reason')->label('Grund')->rows(3), ]) ->action(function (Tenant $record, array $data) { \App\Models\TenantPackage::create([ 'tenant_id' => $record->id, 'package_id' => $data['package_id'], 'expires_at' => $data['expires_at'], 'active' => true, 'reason' => $data['reason'] ?? null, ]); \App\Models\PackagePurchase::create([ 'tenant_id' => $record->id, 'package_id' => $data['package_id'], 'provider' => 'manual', 'provider_id' => 'manual', 'type' => 'reseller_subscription', 'price' => 0, 'metadata' => ['reason' => $data['reason'] ?? 'manual assignment'], ]); }), Actions\Action::make('export') ->label('Daten exportieren') ->icon('heroicon-o-arrow-down-tray') ->url(fn (Tenant $record) => Route::has('admin.tenants.export') ? route('admin.tenants.export', $record) : null) ->visible(fn () => Route::has('admin.tenants.export')) ->openUrlInNewTab(), Actions\ActionGroup::make(static::lifecycleActions()) ->label(__('admin.tenants.actions.lifecycle')) ->icon('heroicon-o-shield-exclamation'), ]) ->bulkActions([ Actions\DeleteBulkAction::make(), ]); } public static function getPages(): array { return [ 'index' => Pages\ListTenants::route('/'), 'view' => Pages\ViewTenant::route('/{record}'), 'lifecycle' => Pages\ViewTenantLifecycle::route('/{record}/lifecycle'), 'edit' => Pages\EditTenant::route('/{record}/edit'), ]; } public static function getRelations(): array { return [ TenantPackagesRelationManager::class, PackagePurchasesRelationManager::class, ]; } /** * @return array */ public static function lifecycleActions(): array { return [ Actions\Action::make('activate') ->label(__('admin.tenants.actions.activate')) ->icon('heroicon-o-check-circle') ->color('success') ->visible(fn (Tenant $record): bool => ! $record->is_active && ! $record->anonymized_at) ->authorize(fn (Tenant $record): bool => Filament::auth()->user()?->can('update', $record) ?? false) ->action(function (Tenant $record): bool { $updated = $record->update(['is_active' => true]); app(TenantLifecycleLogger::class)->record( $record, 'activated', actor: Filament::auth()->user() ); return $updated; }), Actions\Action::make('deactivate') ->label(__('admin.tenants.actions.deactivate')) ->icon('heroicon-o-no-symbol') ->color('danger') ->requiresConfirmation() ->visible(fn (Tenant $record): bool => (bool) $record->is_active && ! $record->anonymized_at) ->authorize(fn (Tenant $record): bool => Filament::auth()->user()?->can('update', $record) ?? false) ->action(function (Tenant $record): bool { $updated = $record->update(['is_active' => false]); app(TenantLifecycleLogger::class)->record( $record, 'deactivated', actor: Filament::auth()->user() ); return $updated; }), Actions\Action::make('suspend') ->label(__('admin.tenants.actions.suspend')) ->icon('heroicon-o-pause-circle') ->color('warning') ->requiresConfirmation() ->visible(fn (Tenant $record): bool => ! $record->is_suspended && ! $record->anonymized_at) ->authorize(fn (Tenant $record): bool => Filament::auth()->user()?->can('suspend', $record) ?? false) ->action(function (Tenant $record): bool { $updated = $record->update(['is_suspended' => true]); app(TenantLifecycleLogger::class)->record( $record, 'suspended', actor: Filament::auth()->user() ); return $updated; }), Actions\Action::make('unsuspend') ->label(__('admin.tenants.actions.unsuspend')) ->icon('heroicon-o-play-circle') ->color('success') ->visible(fn (Tenant $record): bool => (bool) $record->is_suspended && ! $record->anonymized_at) ->authorize(fn (Tenant $record): bool => Filament::auth()->user()?->can('suspend', $record) ?? false) ->action(function (Tenant $record): bool { $updated = $record->update(['is_suspended' => false]); app(TenantLifecycleLogger::class)->record( $record, 'unsuspended', actor: Filament::auth()->user() ); return $updated; }), Actions\Action::make('schedule_deletion') ->label(__('admin.tenants.actions.schedule_deletion')) ->icon('heroicon-o-calendar-days') ->color('warning') ->visible(fn (Tenant $record): bool => ! $record->anonymized_at && $record->pending_deletion_at === null) ->authorize(fn (Tenant $record): bool => Filament::auth()->user()?->can('update', $record) ?? false) ->form([ Forms\Components\DateTimePicker::make('pending_deletion_at') ->label(__('admin.tenants.fields.pending_deletion_at')) ->required() ->minDate(now()->addDay()), Toggle::make('send_warning') ->label(__('admin.tenants.actions.send_warning')) ->default(true), ]) ->action(function (Tenant $record, array $data): void { $plannedDeletion = Carbon::parse($data['pending_deletion_at']); $update = [ 'pending_deletion_at' => $plannedDeletion, ]; if (($data['send_warning'] ?? false) === true) { $email = $record->contact_email ?? $record->email ?? $record->user?->email; if ($email) { NotificationFacade::route('mail', $email) ->notify(new InactiveTenantDeletionWarning($record, $plannedDeletion)); $update['deletion_warning_sent_at'] = now(); } else { Notification::make() ->danger() ->title(__('admin.tenants.actions.send_warning_missing_title')) ->body(__('admin.tenants.actions.send_warning_missing_body')) ->send(); } } $record->forceFill($update)->save(); app(TenantLifecycleLogger::class)->record( $record, 'deletion_scheduled', [ 'pending_deletion_at' => $plannedDeletion->toDateTimeString(), 'send_warning' => (bool) ($data['send_warning'] ?? false), ], Filament::auth()->user() ); }) ->successNotificationTitle(__('admin.tenants.actions.schedule_deletion_success')), Actions\Action::make('cancel_deletion') ->label(__('admin.tenants.actions.cancel_deletion')) ->icon('heroicon-o-x-circle') ->requiresConfirmation() ->visible(fn (Tenant $record): bool => $record->pending_deletion_at !== null && ! $record->anonymized_at) ->authorize(fn (Tenant $record): bool => Filament::auth()->user()?->can('update', $record) ?? false) ->action(function (Tenant $record): void { $previous = $record->pending_deletion_at?->toDateTimeString(); $record->forceFill([ 'pending_deletion_at' => null, 'deletion_warning_sent_at' => null, ])->save(); app(TenantLifecycleLogger::class)->record( $record, 'deletion_cancelled', [ 'pending_deletion_at' => $previous, ], Filament::auth()->user() ); }) ->successNotificationTitle(__('admin.tenants.actions.cancel_deletion_success')), Actions\Action::make('anonymize_now') ->label(__('admin.tenants.actions.anonymize_now')) ->icon('heroicon-o-trash') ->color('danger') ->requiresConfirmation() ->visible(fn (Tenant $record): bool => ! $record->anonymized_at) ->authorize(fn (Tenant $record): bool => Filament::auth()->user()?->can('update', $record) ?? false) ->action(function (Tenant $record): void { AnonymizeAccount::dispatch(null, $record->id); app(TenantLifecycleLogger::class)->record( $record, 'anonymize_requested', actor: Filament::auth()->user() ); }) ->successNotificationTitle(__('admin.tenants.actions.anonymize_success')), ]; } /** * @return array */ public static function lifecycleManagementActions(): array { return [ Actions\Action::make('update_limits') ->label(__('admin.tenants.actions.update_limits')) ->icon('heroicon-o-adjustments-horizontal') ->modalWidth('2xl') ->authorize(fn (Tenant $record): bool => Filament::auth()->user()?->can('update', $record) ?? false) ->form([ TextInput::make('max_photos_per_event') ->label(__('admin.tenants.fields.max_photos_per_event')) ->numeric() ->minValue(0) ->default(fn (Tenant $record) => $record->max_photos_per_event), TextInput::make('max_storage_mb') ->label(__('admin.tenants.fields.max_storage_mb')) ->numeric() ->minValue(0) ->default(fn (Tenant $record) => $record->max_storage_mb), Forms\Components\Textarea::make('note') ->label(__('admin.tenants.actions.note')) ->rows(3), ]) ->action(function (Tenant $record, array $data): void { $before = [ 'max_photos_per_event' => $record->max_photos_per_event, 'max_storage_mb' => $record->max_storage_mb, ]; $record->forceFill([ 'max_photos_per_event' => $data['max_photos_per_event'], 'max_storage_mb' => $data['max_storage_mb'], ])->save(); $after = [ 'max_photos_per_event' => $record->max_photos_per_event, 'max_storage_mb' => $record->max_storage_mb, ]; app(TenantLifecycleLogger::class)->record( $record, 'limits_updated', [ 'before' => $before, 'after' => $after, 'note' => $data['note'] ?? null, ], Filament::auth()->user() ); }), Actions\Action::make('update_subscription_expires_at') ->label(__('admin.tenants.actions.update_subscription_expires_at')) ->icon('heroicon-o-calendar') ->authorize(fn (Tenant $record): bool => Filament::auth()->user()?->can('update', $record) ?? false) ->form([ Forms\Components\DateTimePicker::make('subscription_expires_at') ->label(__('admin.tenants.fields.subscription_expires_at')) ->default(fn (Tenant $record) => $record->subscription_expires_at), Forms\Components\Textarea::make('note') ->label(__('admin.tenants.actions.note')) ->rows(3), ]) ->action(function (Tenant $record, array $data): void { $before = [ 'subscription_expires_at' => optional($record->subscription_expires_at)->toDateTimeString(), ]; $record->forceFill([ 'subscription_expires_at' => $data['subscription_expires_at'], ])->save(); $after = [ 'subscription_expires_at' => optional($record->subscription_expires_at)->toDateTimeString(), ]; app(TenantLifecycleLogger::class)->record( $record, 'subscription_expires_at_updated', [ 'before' => $before, 'after' => $after, 'note' => $data['note'] ?? null, ], Filament::auth()->user() ); }), Actions\Action::make('set_grace_period') ->label(__('admin.tenants.actions.set_grace_period')) ->icon('heroicon-o-clock') ->authorize(fn (Tenant $record): bool => Filament::auth()->user()?->can('update', $record) ?? false) ->form([ Forms\Components\DateTimePicker::make('grace_period_ends_at') ->label(__('admin.tenants.fields.grace_period_ends_at')) ->required() ->minDate(now()) ->default(fn (Tenant $record) => $record->grace_period_ends_at), Forms\Components\Textarea::make('note') ->label(__('admin.tenants.actions.note')) ->rows(3), ]) ->action(function (Tenant $record, array $data): void { $record->forceFill([ 'grace_period_ends_at' => $data['grace_period_ends_at'], ])->save(); app(TenantLifecycleLogger::class)->record( $record, 'grace_period_set', [ 'grace_period_ends_at' => optional($record->grace_period_ends_at)->toDateTimeString(), 'note' => $data['note'] ?? null, ], Filament::auth()->user() ); }), Actions\Action::make('clear_grace_period') ->label(__('admin.tenants.actions.clear_grace_period')) ->icon('heroicon-o-x-circle') ->color('gray') ->requiresConfirmation() ->visible(fn (Tenant $record): bool => $record->grace_period_ends_at !== null) ->authorize(fn (Tenant $record): bool => Filament::auth()->user()?->can('update', $record) ?? false) ->action(function (Tenant $record): void { $previous = $record->grace_period_ends_at?->toDateTimeString(); $record->forceFill([ 'grace_period_ends_at' => null, ])->save(); app(TenantLifecycleLogger::class)->record( $record, 'grace_period_cleared', [ 'grace_period_ends_at' => $previous, ], Filament::auth()->user() ); }), ]; } }