From af0eab3e4774dae9cfd29ea3c695218ed235fe1b Mon Sep 17 00:00:00 2001 From: Simon Hamp Date: Fri, 10 Apr 2026 11:09:11 +0100 Subject: [PATCH] Add developer account details and plugins tab to admin user view Show Stripe Connect account info, onboarding status, and plugin terms when viewing a developer user in admin. Add Developer Plugins relation manager tab and a developer indicator column on the users index table. Co-Authored-By: Claude Opus 4.6 --- app/Filament/Resources/UserResource.php | 47 +++++++ .../DeveloperPluginsRelationManager.php | 49 ++++++++ .../Filament/UserResourceDeveloperTest.php | 116 ++++++++++++++++++ 3 files changed, 212 insertions(+) create mode 100644 app/Filament/Resources/UserResource/RelationManagers/DeveloperPluginsRelationManager.php create mode 100644 tests/Feature/Filament/UserResourceDeveloperTest.php diff --git a/app/Filament/Resources/UserResource.php b/app/Filament/Resources/UserResource.php index e01d18ab..a9f82440 100644 --- a/app/Filament/Resources/UserResource.php +++ b/app/Filament/Resources/UserResource.php @@ -2,6 +2,7 @@ namespace App\Filament\Resources; +use App\Enums\StripeConnectStatus; use App\Filament\Resources\UserResource\Pages; use App\Filament\Resources\UserResource\RelationManagers; use App\Models\User; @@ -12,6 +13,7 @@ use Filament\Schemas\Schema; use Filament\Tables; use Filament\Tables\Table; +use Illuminate\Support\HtmlString; use STS\FilamentImpersonate\Actions\Impersonate; class UserResource extends Resource @@ -62,6 +64,46 @@ public static function form(Schema $schema): Schema ->maxLength(255) ->disabled(), ]), + Schemas\Components\Section::make('Developer Account') + ->inlineLabel() + ->columns(1) + ->visible(fn (?User $record) => $record?->developerAccount !== null) + ->schema([ + Forms\Components\Select::make('developerAccount.stripe_connect_status') + ->label('Stripe Connect Status') + ->options(StripeConnectStatus::class) + ->disabled(), + Forms\Components\Placeholder::make('developerAccount.stripe_connect_account_id') + ->label('Stripe Connect Account') + ->content(fn (User $record) => new HtmlString( + '' + .e($record->developerAccount->stripe_connect_account_id) + .' ↗' + )), + Forms\Components\Placeholder::make('developerAccount.country') + ->label('Country') + ->content(fn (User $record) => $record->developerAccount->country ?? '—'), + Forms\Components\Placeholder::make('developerAccount.payout_currency') + ->label('Payout Currency') + ->content(fn (User $record) => strtoupper($record->developerAccount->payout_currency ?? '—')), + Forms\Components\Placeholder::make('developerAccount.payouts_enabled') + ->label('Payouts Enabled') + ->content(fn (User $record) => $record->developerAccount->payouts_enabled ? 'Yes' : 'No'), + Forms\Components\Placeholder::make('developerAccount.charges_enabled') + ->label('Charges Enabled') + ->content(fn (User $record) => $record->developerAccount->charges_enabled ? 'Yes' : 'No'), + Forms\Components\Placeholder::make('developerAccount.onboarding_completed_at') + ->label('Onboarding Completed') + ->content(fn (User $record) => $record->developerAccount->onboarding_completed_at?->format('M j, Y g:i A') ?? '—'), + Forms\Components\Placeholder::make('developerAccount.accepted_plugin_terms_at') + ->label('Plugin Terms Accepted') + ->content(fn (User $record) => $record->developerAccount->accepted_plugin_terms_at?->format('M j, Y g:i A') ?? '—'), + Forms\Components\Placeholder::make('developerAccount.plugin_terms_version') + ->label('Terms Version') + ->content(fn (User $record) => $record->developerAccount->plugin_terms_version ?? '—'), + ]), ]); } @@ -85,6 +127,10 @@ public static function table(Table $table): Table Tables\Columns\TextColumn::make('anystack_contact_id') ->hidden() ->searchable(), + Tables\Columns\IconColumn::make('developerAccount.id') + ->label('Developer') + ->boolean() + ->getStateUsing(fn (User $record) => $record->developerAccount !== null), Tables\Columns\TextColumn::make('created_at') ->dateTime() ->sortable(), @@ -128,6 +174,7 @@ public static function table(Table $table): Table public static function getRelations(): array { return [ + RelationManagers\DeveloperPluginsRelationManager::class, RelationManagers\PluginLicensesRelationManager::class, RelationManagers\ProductLicensesRelationManager::class, RelationManagers\LicensesRelationManager::class, diff --git a/app/Filament/Resources/UserResource/RelationManagers/DeveloperPluginsRelationManager.php b/app/Filament/Resources/UserResource/RelationManagers/DeveloperPluginsRelationManager.php new file mode 100644 index 00000000..50874aa0 --- /dev/null +++ b/app/Filament/Resources/UserResource/RelationManagers/DeveloperPluginsRelationManager.php @@ -0,0 +1,49 @@ +columns([ + Tables\Columns\TextColumn::make('name') + ->label('Package') + ->searchable() + ->sortable() + ->fontFamily('mono'), + Tables\Columns\TextColumn::make('type') + ->badge() + ->color(fn (PluginType $state): string => match ($state) { + PluginType::Free => 'gray', + PluginType::Paid => 'success', + }), + Tables\Columns\TextColumn::make('status') + ->badge() + ->color(fn (PluginStatus $state): string => $state->color()), + Tables\Columns\TextColumn::make('created_at') + ->label('Submitted') + ->dateTime() + ->sortable(), + ]) + ->defaultSort('created_at', 'desc') + ->recordUrl(fn ($record) => PluginResource::getUrl('edit', ['record' => $record])); + } +} diff --git a/tests/Feature/Filament/UserResourceDeveloperTest.php b/tests/Feature/Filament/UserResourceDeveloperTest.php new file mode 100644 index 00000000..75d52524 --- /dev/null +++ b/tests/Feature/Filament/UserResourceDeveloperTest.php @@ -0,0 +1,116 @@ +admin = User::factory()->create(['email' => 'admin@test.com']); + config(['filament.users' => ['admin@test.com']]); + + $this->user = User::factory()->create(); + } + + public function test_developer_account_section_is_visible_when_user_has_developer_account(): void + { + DeveloperAccount::factory()->create([ + 'user_id' => $this->user->id, + ]); + + $this->actingAs($this->admin) + ->get(EditUser::getUrl(['record' => $this->user])) + ->assertSee('Developer Account') + ->assertSee($this->user->developerAccount->stripe_connect_account_id); + } + + public function test_developer_account_section_is_hidden_when_user_has_no_developer_account(): void + { + $this->actingAs($this->admin) + ->get(EditUser::getUrl(['record' => $this->user])) + ->assertDontSee('Developer Account'); + } + + public function test_developer_account_section_shows_stripe_connect_link(): void + { + $developerAccount = DeveloperAccount::factory()->create([ + 'user_id' => $this->user->id, + ]); + + $this->actingAs($this->admin) + ->get(EditUser::getUrl(['record' => $this->user])) + ->assertSee('https://dashboard.stripe.com/connect/accounts/'.$developerAccount->stripe_connect_account_id); + } + + public function test_developer_plugins_relation_manager_lists_plugins(): void + { + $plugins = Plugin::factory()->count(3)->create([ + 'user_id' => $this->user->id, + ]); + + Livewire::actingAs($this->admin) + ->test(DeveloperPluginsRelationManager::class, [ + 'ownerRecord' => $this->user, + 'pageClass' => EditUser::class, + ]) + ->assertCanSeeTableRecords($plugins) + ->assertCountTableRecords(3); + } + + public function test_developer_plugins_relation_manager_does_not_show_other_users_plugins(): void + { + $otherUser = User::factory()->create(); + + Plugin::factory()->create(['user_id' => $this->user->id]); + Plugin::factory()->create(['user_id' => $otherUser->id]); + + Livewire::actingAs($this->admin) + ->test(DeveloperPluginsRelationManager::class, [ + 'ownerRecord' => $this->user, + 'pageClass' => EditUser::class, + ]) + ->assertCountTableRecords(1); + } + + public function test_developer_plugins_relation_manager_renders_successfully(): void + { + Livewire::actingAs($this->admin) + ->test(DeveloperPluginsRelationManager::class, [ + 'ownerRecord' => $this->user, + 'pageClass' => EditUser::class, + ]) + ->assertOk() + ->assertCountTableRecords(0); + } + + public function test_users_index_shows_developer_column(): void + { + DeveloperAccount::factory()->create([ + 'user_id' => $this->user->id, + ]); + + $nonDeveloper = User::factory()->create(); + + Livewire::actingAs($this->admin) + ->test(UserResource\Pages\ListUsers::class) + ->assertCanRenderTableColumn('developerAccount.id'); + } +}