further rework to the documentation
This commit is contained in:
634
docs/archive/plan-superadmin-filament.md
Normal file
634
docs/archive/plan-superadmin-filament.md
Normal file
@@ -0,0 +1,634 @@
|
||||
# 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. **PAT-Tooling:** Management für Tenant-PATs (TBD)
|
||||
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.
|
||||
Reference in New Issue
Block a user