634 lines
22 KiB
Markdown
634 lines
22 KiB
Markdown
# SuperAdmin Filament Resource Spezifikationen
|
|
|
|
## 1. Erweiterte TenantResource
|
|
|
|
### Form Schema (Erweiterung der bestehenden)
|
|
```php
|
|
// In TenantResource::form()
|
|
TextInput::make('name')->required()->maxLength(255),
|
|
TextInput::make('slug')->required()->unique()->maxLength(255),
|
|
TextInput::make('contact_email')->email()->required()->maxLength(255),
|
|
TextInput::make('event_credits_balance')->numeric()->default(1), // Free tier
|
|
Select::make('subscription_tier')
|
|
->options([
|
|
'free' => 'Free',
|
|
'starter' => 'Starter (€4.99/mo)',
|
|
'pro' => 'Pro (€14.99/mo)',
|
|
'agency' => 'Agency (€19.99/mo)',
|
|
'lifetime' => 'Lifetime (€49.99)'
|
|
])
|
|
->default('free'),
|
|
DateTimePicker::make('subscription_expires_at'),
|
|
TextInput::make('total_revenue')->money('EUR')->readOnly(),
|
|
KeyValue::make('features')->keyLabel('Feature')->valueLabel('Enabled'),
|
|
Toggle::make('is_active')->label('Account Active'),
|
|
Toggle::make('is_suspended')->label('Suspended')->default(false),
|
|
```
|
|
|
|
### Table Columns (Erweiterung)
|
|
```php
|
|
// In TenantResource::table()
|
|
Tables\Columns\TextColumn::make('name')->searchable()->sortable(),
|
|
Tables\Columns\TextColumn::make('slug')->badge()->color('primary'),
|
|
Tables\Columns\TextColumn::make('contact_email')->copyable(),
|
|
Tables\Columns\TextColumn::make('event_credits_balance')
|
|
->label('Credits')
|
|
->badge()
|
|
->color(fn ($state) => $state < 5 ? 'warning' : 'success'),
|
|
Tables\Columns\TextColumn::make('subscription_tier')
|
|
->badge()
|
|
->color(fn (string $state): string => match($state) {
|
|
'free' => 'gray',
|
|
'starter' => 'info',
|
|
'pro' => 'success',
|
|
'agency' => 'warning',
|
|
'lifetime' => 'danger',
|
|
}),
|
|
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\TextColumn::make('last_activity_at')
|
|
->dateTime()
|
|
->sortable()
|
|
->toggleable(),
|
|
Tables\Columns\TextColumn::make('created_at')
|
|
->dateTime()
|
|
->sortable()
|
|
->toggleable(),
|
|
```
|
|
|
|
### Relations Manager (Purchase History)
|
|
```php
|
|
// In TenantResource\RelationManagers\PurchasesRelationManager
|
|
public static function table(Table $table): Table
|
|
{
|
|
return $table
|
|
->recordTitleAttribute('package_id')
|
|
->columns([
|
|
Tables\Columns\TextColumn::make('package_id')->badge(),
|
|
Tables\Columns\TextColumn::make('credits_added')->badge(),
|
|
Tables\Columns\TextColumn::make('price')->money('EUR'),
|
|
Tables\Columns\TextColumn::make('platform')->badge(),
|
|
Tables\Columns\TextColumn::make('purchased_at')->dateTime(),
|
|
Tables\Columns\TextColumn::make('transaction_id')->copyable(),
|
|
])
|
|
->filters([
|
|
Tables\Filters\SelectFilter::make('platform')
|
|
->options(['ios' => 'iOS', 'android' => 'Android', 'web' => 'Web']),
|
|
Tables\Filters\SelectFilter::make('package_id')
|
|
->options(['starter' => 'Starter', 'pro' => 'Pro', 'lifetime' => 'Lifetime']),
|
|
])
|
|
->headerActions([])
|
|
->actions([Tables\Actions\ViewAction::make()])
|
|
->bulkActions([Tables\Actions\BulkActionGroup::make([])]);
|
|
}
|
|
```
|
|
|
|
### Actions
|
|
```php
|
|
// In TenantResource::table()
|
|
->actions([
|
|
Actions\ViewAction::make(),
|
|
Actions\EditAction::make(),
|
|
Actions\Action::make('add_credits')
|
|
->label('Credits hinzufügen')
|
|
->icon('heroicon-o-plus')
|
|
->form([
|
|
Forms\Components\TextInput::make('credits')->numeric()->required()->minValue(1),
|
|
Forms\Components\Textarea::make('reason')->label('Grund')->rows(3),
|
|
])
|
|
->action(function (Tenant $record, array $data) {
|
|
$record->increment('event_credits_balance', $data['credits']);
|
|
// Log purchase_history entry
|
|
PurchaseHistory::create([
|
|
'tenant_id' => $record->id,
|
|
'package_id' => 'manual_adjustment',
|
|
'credits_added' => $data['credits'],
|
|
'price' => 0,
|
|
'reason' => $data['reason'],
|
|
]);
|
|
}),
|
|
Actions\Action::make('suspend')
|
|
->label('Suspendieren')
|
|
->color('danger')
|
|
->requiresConfirmation()
|
|
->action(fn (Tenant $record) => $record->update(['is_suspended' => true])),
|
|
Actions\Action::make('export')
|
|
->label('Daten exportieren')
|
|
->icon('heroicon-o-arrow-down-tray')
|
|
->url(fn (Tenant $record) => route('admin.tenants.export', $record))
|
|
->openUrlInNewTab(),
|
|
])
|
|
```
|
|
|
|
## 2. Neue PurchaseHistoryResource
|
|
|
|
### Model (app/Models/PurchaseHistory.php)
|
|
```php
|
|
class PurchaseHistory extends Model
|
|
{
|
|
protected $table = 'purchase_history';
|
|
protected $guarded = [];
|
|
protected $casts = [
|
|
'price' => 'decimal:2',
|
|
'purchased_at' => 'datetime',
|
|
];
|
|
|
|
public function tenant(): BelongsTo
|
|
{
|
|
return $this->belongsTo(Tenant::class);
|
|
}
|
|
|
|
public function scopeByTenant($query, $tenantId)
|
|
{
|
|
return $query->where('tenant_id', $tenantId);
|
|
}
|
|
|
|
public function scopeByPlatform($query, $platform)
|
|
{
|
|
return $query->where('platform', $platform);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Resource (app/Filament/Resources/PurchaseHistoryResource.php)
|
|
```php
|
|
class PurchaseHistoryResource extends Resource
|
|
{
|
|
protected static ?string $model = PurchaseHistory::class;
|
|
protected static ?string $navigationIcon = 'heroicon-o-shopping-cart';
|
|
protected static ?string $navigationGroup = 'Billing';
|
|
protected static ?int $navigationSort = 10;
|
|
|
|
public static function form(Form $form): Form
|
|
{
|
|
return $form
|
|
->schema([
|
|
Select::make('tenant_id')
|
|
->label('Tenant')
|
|
->relationship('tenant', 'name')
|
|
->searchable()
|
|
->preload()
|
|
->required(),
|
|
Select::make('package_id')
|
|
->label('Paket')
|
|
->options([
|
|
'starter_pack' => 'Starter Pack (5 Events)',
|
|
'pro_pack' => 'Pro Pack (20 Events)',
|
|
'lifetime_unlimited' => 'Lifetime Unlimited',
|
|
'monthly_pro' => 'Pro Subscription',
|
|
'monthly_agency' => 'Agency Subscription',
|
|
])
|
|
->required(),
|
|
TextInput::make('credits_added')
|
|
->label('Credits hinzugefügt')
|
|
->numeric()
|
|
->required()
|
|
->minValue(0),
|
|
TextInput::make('price')
|
|
->label('Preis')
|
|
->money('EUR')
|
|
->required(),
|
|
Select::make('platform')
|
|
->label('Plattform')
|
|
->options([
|
|
'ios' => 'iOS',
|
|
'android' => 'Android',
|
|
'web' => 'Web',
|
|
'manual' => 'Manuell',
|
|
])
|
|
->required(),
|
|
TextInput::make('transaction_id')
|
|
->label('Transaktions-ID')
|
|
->maxLength(255),
|
|
Textarea::make('reason')
|
|
->label('Beschreibung')
|
|
->maxLength(65535)
|
|
->columnSpanFull(),
|
|
])
|
|
->columns(2);
|
|
}
|
|
|
|
public static function table(Table $table): Table
|
|
{
|
|
return $table
|
|
->columns([
|
|
Tables\Columns\TextColumn::make('tenant.name')
|
|
->label('Tenant')
|
|
->searchable()
|
|
->sortable(),
|
|
Tables\Columns\TextColumn::make('package_id')
|
|
->label('Paket')
|
|
->badge()
|
|
->color(fn (string $state): string => match($state) {
|
|
'starter_pack' => 'info',
|
|
'pro_pack' => 'success',
|
|
'lifetime_unlimited' => 'danger',
|
|
'monthly_pro' => 'warning',
|
|
default => 'gray',
|
|
}),
|
|
Tables\Columns\TextColumn::make('credits_added')
|
|
->label('Credits')
|
|
->badge()
|
|
->color('success'),
|
|
Tables\Columns\TextColumn::make('price')
|
|
->label('Preis')
|
|
->money('EUR')
|
|
->sortable(),
|
|
Tables\Columns\TextColumn::make('platform')
|
|
->badge()
|
|
->color(fn (string $state): string => match($state) {
|
|
'ios' => 'info',
|
|
'android' => 'success',
|
|
'web' => 'warning',
|
|
'manual' => 'gray',
|
|
}),
|
|
Tables\Columns\TextColumn::make('purchased_at')
|
|
->dateTime()
|
|
->sortable(),
|
|
Tables\Columns\TextColumn::make('transaction_id')
|
|
->copyable()
|
|
->toggleable(),
|
|
])
|
|
->filters([
|
|
Tables\Filters\Filter::make('created_at')
|
|
->form([
|
|
Forms\Components\DatePicker::make('started_from'),
|
|
Forms\Components\DatePicker::make('ended_before'),
|
|
])
|
|
->query(function ($query, array $data) {
|
|
return $query
|
|
->when($data['started_from'], fn ($q) => $q->whereDate('created_at', '>=', $data['started_from']))
|
|
->when($data['ended_before'], fn ($q) => $q->whereDate('created_at', '<=', $data['ended_before']));
|
|
}),
|
|
Tables\Filters\SelectFilter::make('platform')
|
|
->options([
|
|
'ios' => 'iOS',
|
|
'android' => 'Android',
|
|
'web' => 'Web',
|
|
'manual' => 'Manuell',
|
|
]),
|
|
Tables\Filters\SelectFilter::make('package_id')
|
|
->options([
|
|
'starter_pack' => 'Starter Pack',
|
|
'pro_pack' => 'Pro Pack',
|
|
'lifetime_unlimited' => 'Lifetime',
|
|
'monthly_pro' => 'Pro Subscription',
|
|
'monthly_agency' => 'Agency Subscription',
|
|
]),
|
|
Tables\Filters\TernaryFilter::make('successful')
|
|
->label('Erfolgreich')
|
|
->trueLabel('Ja')
|
|
->falseLabel('Nein')
|
|
->placeholder('Alle')
|
|
->query(fn ($query) => $query->whereNotNull('transaction_id')),
|
|
])
|
|
->actions([
|
|
Tables\Actions\ViewAction::make(),
|
|
Tables\Actions\Action::make('refund')
|
|
->label('Rückerstattung')
|
|
->color('danger')
|
|
->icon('heroicon-o-arrow-uturn-left')
|
|
->requiresConfirmation()
|
|
->visible(fn ($record) => $record->transaction_id && !$record->refunded)
|
|
->action(function (PurchaseHistory $record) {
|
|
// Stripe/RevenueCat Refund API call
|
|
$record->update(['refunded' => true, 'refunded_at' => now()]);
|
|
$record->tenant->decrement('event_credits_balance', $record->credits_added);
|
|
}),
|
|
])
|
|
->bulkActions([
|
|
Tables\Actions\BulkActionGroup::make([
|
|
Tables\Actions\DeleteBulkAction::make(),
|
|
ExportBulkAction::make()
|
|
->label('Export CSV')
|
|
->exporter(PurchaseHistoryExporter::class),
|
|
]),
|
|
])
|
|
->emptyStateHeading('Keine Käufe gefunden')
|
|
->emptyStateDescription('Erstelle den ersten Kauf oder überprüfe die Filter.');
|
|
}
|
|
}
|
|
```
|
|
|
|
## 3. OAuthClientResource (für SuperAdmin)
|
|
|
|
### Resource (app/Filament/Resources/OAuthClientResource.php)
|
|
```php
|
|
class OAuthClientResource extends Resource
|
|
{
|
|
protected static ?string $model = OAuthClient::class;
|
|
protected static ?string $navigationIcon = 'heroicon-o-key';
|
|
protected static ?string $navigationGroup = 'Security';
|
|
protected static ?int $navigationSort = 20;
|
|
|
|
public static function form(Form $form): Form
|
|
{
|
|
return $form
|
|
->schema([
|
|
TextInput::make('client_id')
|
|
->label('Client ID')
|
|
->required()
|
|
->unique(ignoreRecord: true)
|
|
->maxLength(255),
|
|
TextInput::make('client_secret')
|
|
->label('Client Secret')
|
|
->password()
|
|
->required()
|
|
->maxLength(255)
|
|
->dehydrateStateUsing(fn ($state) => Hash::make($state)),
|
|
TextInput::make('name')
|
|
->label('Name')
|
|
->required()
|
|
->maxLength(255),
|
|
Textarea::make('redirect_uris')
|
|
->label('Redirect URIs')
|
|
->rows(3)
|
|
->placeholder('https://tenant-admin-app.example.com/callback')
|
|
->required(),
|
|
KeyValue::make('scopes')
|
|
->label('Scopes')
|
|
->keyLabel('Scope')
|
|
->valueLabel('Beschreibung')
|
|
->default([
|
|
'tenant:read' => 'Lesen von Tenant-Daten',
|
|
'tenant:write' => 'Schreiben von Tenant-Daten',
|
|
'tenant:admin' => 'Administrative Aktionen',
|
|
]),
|
|
Toggle::make('is_active')
|
|
->label('Aktiv')
|
|
->default(true),
|
|
Textarea::make('description')
|
|
->label('Beschreibung')
|
|
->maxLength(65535)
|
|
->columnSpanFull(),
|
|
])
|
|
->columns(2);
|
|
}
|
|
|
|
public static function table(Table $table): Table
|
|
{
|
|
return $table
|
|
->columns([
|
|
Tables\Columns\TextColumn::make('name')
|
|
->searchable()
|
|
->sortable(),
|
|
Tables\Columns\TextColumn::make('client_id')
|
|
->copyable()
|
|
->sortable(),
|
|
Tables\Columns\IconColumn::make('is_active')
|
|
->boolean()
|
|
->color('success'),
|
|
Tables\Columns\TextColumn::make('redirect_uris')
|
|
->limit(50)
|
|
->tooltip(function (Tables\Columns\Column $column): ?string {
|
|
$state = $column->getState();
|
|
if (is_array($state)) {
|
|
return implode("\n", $state);
|
|
}
|
|
return $state;
|
|
}),
|
|
Tables\Columns\TextColumn::make('scopes')
|
|
->badge()
|
|
->color('info'),
|
|
Tables\Columns\TextColumn::make('created_at')
|
|
->dateTime()
|
|
->sortable()
|
|
->toggleable(),
|
|
])
|
|
->filters([
|
|
Tables\Filters\TernaryFilter::make('is_active')
|
|
->label('Status')
|
|
->trueLabel('Aktiv')
|
|
->falseLabel('Inaktiv')
|
|
->placeholder('Alle'),
|
|
])
|
|
->actions([
|
|
Tables\Actions\ViewAction::make(),
|
|
Tables\Actions\EditAction::make(),
|
|
Tables\Actions\Action::make('regenerate_secret')
|
|
->label('Secret neu generieren')
|
|
->icon('heroicon-o-arrow-path')
|
|
->color('warning')
|
|
->requiresConfirmation()
|
|
->action(function (OAuthClient $record) {
|
|
$record->update(['client_secret' => Str::random(40)]);
|
|
}),
|
|
Tables\Actions\DeleteAction::make()
|
|
->requiresConfirmation()
|
|
->before(function (OAuthClient $record) {
|
|
// Revoke all associated tokens
|
|
RefreshToken::where('client_id', $record->client_id)->delete();
|
|
}),
|
|
])
|
|
->bulkActions([
|
|
Tables\Actions\BulkActionGroup::make([
|
|
Tables\Actions\DeleteBulkAction::make(),
|
|
]),
|
|
]);
|
|
}
|
|
}
|
|
```
|
|
|
|
## 4. SuperAdmin Dashboard Widgets
|
|
|
|
### RevenueChart Widget
|
|
```php
|
|
// app/Filament/Widgets/RevenueChart.php
|
|
class RevenueChart extends LineChartWidget
|
|
{
|
|
protected static ?string $heading = 'Monatliche Einnahmen';
|
|
protected static ?int $sort = 1;
|
|
|
|
protected function getData(): array
|
|
{
|
|
$data = PurchaseHistory::selectRaw('
|
|
DATE_FORMAT(purchased_at, "%Y-%m") as month,
|
|
SUM(price) as revenue
|
|
')
|
|
->groupBy('month')
|
|
->orderBy('month')
|
|
->limit(12)
|
|
->get();
|
|
|
|
return [
|
|
'datasets' => [
|
|
[
|
|
'label' => 'Einnahmen (€)',
|
|
'data' => $data->pluck('revenue')->values(),
|
|
'borderColor' => '#3B82F6',
|
|
'backgroundColor' => 'rgba(59, 130, 246, 0.1)',
|
|
],
|
|
],
|
|
'labels' => $data->pluck('month')->values(),
|
|
];
|
|
}
|
|
|
|
protected function getFilters(): ?array
|
|
{
|
|
return [
|
|
'12_months' => 'Letzte 12 Monate',
|
|
'6_months' => 'Letzte 6 Monate',
|
|
'3_months' => 'Letzte 3 Monate',
|
|
];
|
|
}
|
|
}
|
|
```
|
|
|
|
### TopTenantsWidget
|
|
```php
|
|
// app/Filament/Widgets/TopTenantsByRevenue.php
|
|
class TopTenantsByRevenue extends TableWidget
|
|
{
|
|
protected static ?string $heading = 'Top Tenants nach Einnahmen';
|
|
protected static ?int $sort = 2;
|
|
|
|
protected function getTableQuery(): Builder
|
|
{
|
|
return Tenant::withCount('purchases')
|
|
->withSum('purchases', 'price')
|
|
->orderByDesc('purchases_sum_price')
|
|
->limit(10);
|
|
}
|
|
|
|
protected function getTableColumns(): array
|
|
{
|
|
return [
|
|
Tables\Columns\TextColumn::make('name')
|
|
->label('Tenant')
|
|
->searchable()
|
|
->sortable(),
|
|
Tables\Columns\TextColumn::make('purchases_sum_price')
|
|
->label('Gesamt (€)')
|
|
->money('EUR')
|
|
->sortable(),
|
|
Tables\Columns\TextColumn::make('purchases_count')
|
|
->label('Käufe')
|
|
->badge()
|
|
->sortable(),
|
|
Tables\Columns\TextColumn::make('event_credits_balance')
|
|
->label('Aktuelle Credits')
|
|
->badge(),
|
|
];
|
|
}
|
|
}
|
|
```
|
|
|
|
### CreditAlertsWidget
|
|
```php
|
|
// app/Filament/Widgets/CreditAlerts.php
|
|
class CreditAlerts extends StatsOverviewWidget
|
|
{
|
|
protected function getCards(): array
|
|
{
|
|
$lowBalanceTenants = Tenant::where('event_credits_balance', '<', 5)
|
|
->where('is_active', true)
|
|
->count();
|
|
|
|
$totalRevenueThisMonth = PurchaseHistory::whereMonth('purchased_at', now()->month)
|
|
->sum('price');
|
|
|
|
$activeSubscriptions = Tenant::whereNotNull('subscription_expires_at')
|
|
->where('subscription_expires_at', '>', now())
|
|
->count();
|
|
|
|
return [
|
|
StatsOverviewWidget\Stat::make('Tenants mit niedrigen Credits', $lowBalanceTenants)
|
|
->description('Benötigen möglicherweise Support')
|
|
->descriptionIcon('heroicon-m-exclamation-triangle')
|
|
->color('warning')
|
|
->url(route('filament.admin.resources.tenants.index')),
|
|
StatsOverviewWidget\Stat::make('Einnahmen diesen Monat', $totalRevenueThisMonth)
|
|
->description('€')
|
|
->descriptionIcon('heroicon-m-currency-euro')
|
|
->color('success'),
|
|
StatsOverviewWidget\Stat::make('Aktive Abos', $activeSubscriptions)
|
|
->description('Recurring Revenue')
|
|
->descriptionIcon('heroicon-m-arrow-trending-up')
|
|
->color('info'),
|
|
];
|
|
}
|
|
}
|
|
```
|
|
|
|
## 5. Permissions und Middleware
|
|
|
|
### Policy für SuperAdmin
|
|
```php
|
|
// app/Policies/TenantPolicy.php
|
|
class TenantPolicy
|
|
{
|
|
public function before(User $user): ?bool
|
|
{
|
|
if ($user->role === 'superadmin') {
|
|
return true;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public function viewAny(User $user): bool
|
|
{
|
|
return $user->role === 'superadmin';
|
|
}
|
|
|
|
public function view(User $user, Tenant $tenant): bool
|
|
{
|
|
return $user->role === 'superadmin' || $user->tenant_id === $tenant->id;
|
|
}
|
|
|
|
public function create(User $user): bool
|
|
{
|
|
return $user->role === 'superadmin';
|
|
}
|
|
|
|
public function update(User $user, Tenant $tenant): bool
|
|
{
|
|
return $user->role === 'superadmin';
|
|
}
|
|
|
|
public function delete(User $user, Tenant $tenant): bool
|
|
{
|
|
return $user->role === 'superadmin';
|
|
}
|
|
|
|
public function addCredits(User $user, Tenant $tenant): bool
|
|
{
|
|
return $user->role === 'superadmin';
|
|
}
|
|
|
|
public function suspend(User $user, Tenant $tenant): bool
|
|
{
|
|
return $user->role === 'superadmin';
|
|
}
|
|
}
|
|
```
|
|
|
|
### SuperAdmin Middleware
|
|
```php
|
|
// app/Http/Middleware/SuperAdminMiddleware.php
|
|
class SuperAdminMiddleware
|
|
{
|
|
public function handle(Request $request, Closure $next)
|
|
{
|
|
if (!auth()->check() || auth()->user()->role !== 'superadmin') {
|
|
abort(403, 'SuperAdmin-Zugriff erforderlich');
|
|
}
|
|
|
|
return $next($request);
|
|
}
|
|
}
|
|
```
|
|
|
|
## Implementierungsreihenfolge
|
|
1. **Model & Migration:** PurchaseHistory, OAuthClient Tabellen
|
|
2. **TenantResource:** Erweiterung mit neuen Feldern, Relations, Actions
|
|
3. **PurchaseHistoryResource:** Vollständige CRUD mit Filtern und Export
|
|
4. **OAuthClientResource:** Management für OAuth-Clients
|
|
5. **Widgets:** Dashboard-Übersicht mit Charts und Stats
|
|
6. **Policies & Middleware:** Security für SuperAdmin-Funktionen
|
|
7. **Tests:** Feature-Tests für Credit-Management, Permissions
|
|
|
|
Dieser Plan erweitert den SuperAdmin-Bereich um umfassende Billing- und Security-Management-Funktionen. |