From e911c2bd1653f39ee94fa63a3007752f9b31b0f2 Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Wed, 4 Feb 2026 16:03:51 +0100 Subject: [PATCH] Add join token expiry action in event modal --- app/Filament/Resources/EventResource.php | 113 +++++++++++++++++- resources/lang/de/admin.php | 8 ++ resources/lang/en/admin.php | 8 ++ .../views/filament/events/join-link.blade.php | 3 + .../EventJoinTokenExpiryActionTest.php | 64 ++++++++++ 5 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 tests/Feature/EventJoinTokenExpiryActionTest.php diff --git a/app/Filament/Resources/EventResource.php b/app/Filament/Resources/EventResource.php index af266ca8..3c04884e 100644 --- a/app/Filament/Resources/EventResource.php +++ b/app/Filament/Resources/EventResource.php @@ -6,19 +6,23 @@ use App\Filament\Clusters\DailyOps\DailyOpsCluster; use App\Filament\Resources\EventResource\Pages; use App\Filament\Resources\EventResource\RelationManagers\EventPackagesRelationManager; use App\Models\Event; +use App\Models\EventJoinToken; use App\Models\EventJoinTokenEvent; use App\Models\EventType; use App\Models\Tenant; use App\Services\Audit\SuperAdminAuditLogger; +use App\Services\EventJoinTokenService; use App\Support\JoinTokenLayoutRegistry; use BackedEnum; use Carbon\Carbon; use Filament\Actions; use Filament\Forms\Components\DatePicker; +use Filament\Forms\Components\DateTimePicker; use Filament\Forms\Components\KeyValue; use Filament\Forms\Components\Select; use Filament\Forms\Components\TextInput; use Filament\Forms\Components\Toggle; +use Filament\Notifications\Notification; use Filament\Resources\Resource; use Filament\Schemas\Schema; use Filament\Tables; @@ -164,7 +168,100 @@ class EventResource extends Resource ->modalHeading(__('admin.events.modal.join_link_heading')) ->modalSubmitActionLabel(__('admin.common.close')) ->modalWidth('xl') - ->modalContent(function ($record) { + ->registerModalActions([ + Actions\Action::make('extend_join_token_expiry') + ->label(__('admin.events.join_link.extend_expiry')) + ->icon('heroicon-o-clock') + ->color('warning') + ->size('xs') + ->modalHeading(function (Actions\Action $action, Event $record): string { + $token = static::resolveJoinTokenFromAction($record, $action); + + return $token + ? __('admin.events.join_link.extend_expiry_heading', [ + 'label' => $token->label ?: __('admin.events.join_link.token_default', ['id' => $token->id]), + ]) + : __('admin.events.join_link.extend_expiry_heading_fallback'); + }) + ->schema(function (Event $record): array { + $minimumExpiry = app(EventJoinTokenService::class)->minimumExpiryForEvent($record); + $rules = [ + 'date', + 'after:now', + ]; + + if ($minimumExpiry) { + $rules[] = 'after_or_equal:'.$minimumExpiry->toDateTimeString(); + } + + return [ + DateTimePicker::make('expires_at') + ->label(__('admin.events.join_link.extend_expiry_label')) + ->required() + ->seconds(false) + ->rules($rules) + ->helperText($minimumExpiry + ? __('admin.events.join_link.extend_expiry_min', [ + 'date' => $minimumExpiry->isoFormat('LLL'), + ]) + : null), + ]; + }) + ->fillForm(function (Actions\Action $action, Event $record): array { + $token = static::resolveJoinTokenFromAction($record, $action); + + if (! $token) { + return []; + } + + return [ + 'expires_at' => $token->expires_at, + ]; + }) + ->action(function (array $data, Actions\Action $action, Event $record): void { + $token = static::resolveJoinTokenFromAction($record, $action); + + if (! $token) { + Notification::make() + ->title(__('admin.events.join_link.extend_expiry_missing')) + ->danger() + ->send(); + + return; + } + + $expiresAt = $data['expires_at'] ?? null; + + if (! $expiresAt) { + Notification::make() + ->title(__('admin.events.join_link.extend_expiry_missing_date')) + ->danger() + ->send(); + + return; + } + + $resolvedExpiry = $expiresAt instanceof Carbon + ? $expiresAt + : Carbon::parse($expiresAt); + + $token->forceFill([ + 'expires_at' => $resolvedExpiry, + ])->save(); + + app(SuperAdminAuditLogger::class)->recordModelMutation( + 'updated', + $token, + source: static::class + ); + + Notification::make() + ->title(__('admin.events.join_link.extend_expiry_success')) + ->success() + ->send(); + }), + ]) + ->modalContent(function (Actions\Action $action, $record) { $tokens = $record->joinTokens() ->orderByDesc('created_at') ->get(); @@ -253,6 +350,7 @@ class EventResource extends Resource return view('filament.events.join-link', [ 'event' => $record, 'tokens' => $tokens, + 'action' => $action, ]); }), ]) @@ -303,6 +401,19 @@ class EventResource extends Resource return is_string($name) ? $name : ''; } + private static function resolveJoinTokenFromAction(Event $record, Actions\Action $action): ?EventJoinToken + { + $tokenId = $action->getArguments()['token_id'] ?? null; + + if (! $tokenId) { + return null; + } + + return $record->joinTokens() + ->whereKey($tokenId) + ->first(); + } + public static function getPages(): array { return [ diff --git a/resources/lang/de/admin.php b/resources/lang/de/admin.php index 4c3fc448..a69dd57f 100644 --- a/resources/lang/de/admin.php +++ b/resources/lang/de/admin.php @@ -407,6 +407,14 @@ return [ 'layouts_heading' => 'Drucklayouts', 'layouts_fallback' => 'Layout-Übersicht öffnen', 'token_expiry' => 'Läuft ab am :date', + 'extend_expiry' => 'Ablauf verlängern', + 'extend_expiry_label' => 'Neues Ablaufdatum', + 'extend_expiry_heading' => 'Ablauf für :label verlängern', + 'extend_expiry_heading_fallback' => 'Ablauf der Einladung verlängern', + 'extend_expiry_min' => 'Mindestlaufzeit: :date', + 'extend_expiry_missing' => 'Einladung nicht gefunden.', + 'extend_expiry_missing_date' => 'Bitte ein neues Ablaufdatum wählen.', + 'extend_expiry_success' => 'Ablauf der Einladung aktualisiert.', ], 'analytics' => [ 'success_total' => 'Erfolgreiche Zugriffe', diff --git a/resources/lang/en/admin.php b/resources/lang/en/admin.php index 182e3d00..c7460fff 100644 --- a/resources/lang/en/admin.php +++ b/resources/lang/en/admin.php @@ -402,6 +402,14 @@ return [ 'layouts_heading' => 'Printable layouts', 'layouts_fallback' => 'Open layout overview', 'token_expiry' => 'Expires at :date', + 'extend_expiry' => 'Extend expiry', + 'extend_expiry_label' => 'New expiry', + 'extend_expiry_heading' => 'Extend expiry for :label', + 'extend_expiry_heading_fallback' => 'Extend invitation expiry', + 'extend_expiry_min' => 'Minimum expiry: :date', + 'extend_expiry_missing' => 'Invitation not found.', + 'extend_expiry_missing_date' => 'Please select a new expiry.', + 'extend_expiry_success' => 'Invitation expiry updated.', 'deprecated_notice' => 'Direct access via slug :slug has been retired. Share the invitations below or manage QR layouts in the admin app.', 'open_admin' => 'Open admin app', ], diff --git a/resources/views/filament/events/join-link.blade.php b/resources/views/filament/events/join-link.blade.php index 2ce91de1..256839f8 100644 --- a/resources/views/filament/events/join-link.blade.php +++ b/resources/views/filament/events/join-link.blade.php @@ -76,6 +76,9 @@ > {{ __('admin.events.join_link.copy_link') }} + @if (isset($action)) + {{ $action->getModalAction('extend_join_token_expiry')(['token_id' => $token['id']]) }} + @endif diff --git a/tests/Feature/EventJoinTokenExpiryActionTest.php b/tests/Feature/EventJoinTokenExpiryActionTest.php new file mode 100644 index 00000000..18314ff9 --- /dev/null +++ b/tests/Feature/EventJoinTokenExpiryActionTest.php @@ -0,0 +1,64 @@ +create(['role' => 'super_admin']); + $event = Event::factory()->create([ + 'date' => now()->addDays(10), + ]); + + $token = $event->joinTokens()->latest('id')->first(); + + $minimumExpiry = app(EventJoinTokenService::class)->minimumExpiryForEvent($event); + $newExpiry = ($minimumExpiry ?? now()->addDay())->copy()->addDays(2)->seconds(0); + + $this->bootSuperAdminPanel($user); + + Livewire::test(ListEvents::class) + ->callAction( + [ + TestAction::make('join_tokens')->table($event), + TestAction::make('extend_join_token_expiry') + ->arguments(['token_id' => $token->id]), + ], + [ + 'expires_at' => $newExpiry->toDateTimeString(), + ] + ) + ->assertHasNoErrors(); + + $token->refresh(); + + $this->assertSame( + $newExpiry->toDateTimeString(), + $token->expires_at?->toDateTimeString() + ); + } + + private function bootSuperAdminPanel(User $user): void + { + $panel = Filament::getPanel('superadmin'); + + $this->assertNotNull($panel); + + Filament::setCurrentPanel($panel); + Filament::bootCurrentPanel(); + Filament::auth()->login($user); + } +}