schema([ Select::make('tenant_id') ->label(__('admin.events.fields.tenant')) ->options(Tenant::query()->pluck('name', 'id')) ->searchable() ->required(), TextInput::make('name.de') ->label(__('admin.events.fields.name')) ->required() ->maxLength(255), TextInput::make('slug') ->label(__('admin.events.fields.slug')) ->required() ->unique(ignoreRecord: true) ->maxLength(255), TextInput::make('join_link_display') ->label(__('admin.events.fields.join_link')) ->afterStateHydrated(function (TextInput $component, ?Event $record) { if (! $record) { return; } $token = $record->joinTokens()->latest()->first(); $component->state($token ? url('/e/'.$token->token) : '-'); }) ->readOnly() ->dehydrated(false) ->visibleOn('edit'), DatePicker::make('date') ->label(__('admin.events.fields.date')) ->required(), Select::make('event_type_id') ->label(__('admin.events.fields.type')) ->options(fn () => EventType::all()->pluck('name.de', 'id')) ->searchable(), Select::make('package_id') ->label(__('admin.events.fields.package')) ->options(\App\Models\Package::query()->where('type', 'endcustomer')->pluck('name', 'id')) ->searchable() ->preload() ->required() ->visibleOn('create'), TextInput::make('default_locale') ->label(__('admin.events.fields.default_locale')) ->default('de') ->maxLength(5), Toggle::make('is_active') ->label(__('admin.events.fields.is_active')) ->default(true), KeyValue::make('settings') ->label(__('admin.events.fields.settings')) ->keyLabel(__('admin.common.key')) ->valueLabel(__('admin.common.value')) ->addButtonLabel(__('admin.common.add')) ->reorderable() ->columnSpanFull(), ])->columns(2); } public static function table(Table $table): Table { return $table ->columns([ Tables\Columns\TextColumn::make('id')->sortable(), Tables\Columns\TextColumn::make('tenant.name')->label(__('admin.events.table.tenant'))->searchable(), Tables\Columns\TextColumn::make('name') ->label(__('admin.events.fields.name')) ->formatStateUsing(fn (mixed $state): string => static::formatEventName($state)) ->limit(30), Tables\Columns\TextColumn::make('slug')->searchable(), Tables\Columns\TextColumn::make('date')->date(), Tables\Columns\IconColumn::make('is_active')->boolean(), Tables\Columns\TextColumn::make('eventPackage.package.name') ->label(__('admin.events.table.package')) ->badge() ->color('success'), Tables\Columns\TextColumn::make('eventPackage.used_photos') ->label(__('admin.events.table.used_photos')) ->badge(), Tables\Columns\TextColumn::make('eventPackage.remaining_photos') ->label(__('admin.events.table.remaining_photos')) ->badge() ->color(fn ($state) => $state < 1 ? 'danger' : 'success') ->getStateUsing(fn ($record) => $record->eventPackage?->remaining_photos ?? 0), Tables\Columns\TextColumn::make('created_at')->since(), ]) ->filters([]) ->actions([ Actions\EditAction::make() ->after(fn (array $data, Event $record) => app(SuperAdminAuditLogger::class)->recordModelMutation( 'updated', $record, SuperAdminAuditLogger::fieldsMetadata($data), static::class )), Actions\Action::make('toggle') ->label(__('admin.events.actions.toggle_active')) ->icon('heroicon-o-power') ->action(function (Event $record): void { $record->update(['is_active' => ! $record->is_active]); app(SuperAdminAuditLogger::class)->record( 'event.toggled', $record, SuperAdminAuditLogger::fieldsMetadata(['is_active']), source: static::class ); }), Actions\Action::make('download_photos') ->label(__('admin.events.actions.download_photos')) ->icon('heroicon-o-arrow-down-tray') ->url(fn (Event $record) => route('tenant.events.photos.archive', $record)) ->openUrlInNewTab() ->visible(fn (Event $record) => $record->photos()->where('status', 'approved')->exists()), Actions\Action::make('join_tokens') ->label(__('admin.events.actions.join_link_qr')) ->icon('heroicon-o-qr-code') ->modalHeading(__('admin.events.modal.join_link_heading')) ->modalSubmitActionLabel(__('admin.common.close')) ->modalWidth('xl') ->modalContent(function ($record) { $tokens = $record->joinTokens() ->orderByDesc('created_at') ->get(); if ($tokens->isEmpty()) { return view('filament.events.join-link', [ 'event' => $record, 'tokens' => collect(), ]); } $tokenIds = $tokens->pluck('id'); $now = now(); $totals = EventJoinTokenEvent::query() ->selectRaw('event_join_token_id, event_type, COUNT(*) as total') ->whereIn('event_join_token_id', $tokenIds) ->groupBy('event_join_token_id', 'event_type') ->get() ->groupBy('event_join_token_id'); $recent24h = EventJoinTokenEvent::query() ->selectRaw('event_join_token_id, COUNT(*) as total') ->whereIn('event_join_token_id', $tokenIds) ->where('occurred_at', '>=', $now->copy()->subHours(24)) ->groupBy('event_join_token_id') ->pluck('total', 'event_join_token_id'); $lastSeen = EventJoinTokenEvent::query() ->whereIn('event_join_token_id', $tokenIds) ->selectRaw('event_join_token_id, MAX(occurred_at) as last_at') ->groupBy('event_join_token_id') ->pluck('last_at', 'event_join_token_id'); $tokens = $tokens->map(function ($token) use ($record, $totals, $recent24h, $lastSeen) { $layouts = JoinTokenLayoutRegistry::toResponse(function (string $layoutId, string $format) use ($record, $token) { return route('api.v1.tenant.events.join-tokens.layouts.download', [ 'event' => $record->slug, 'joinToken' => $token->getKey(), 'layout' => $layoutId, 'format' => $format, ]); }); $analyticsGroup = $totals->get($token->id, collect()); $analytics = $analyticsGroup->mapWithKeys(function ($row) { return [$row->event_type => (int) $row->total]; }); $successCount = (int) ($analytics['access_granted'] ?? 0) + (int) ($analytics['gallery_access_granted'] ?? 0); $failureCount = (int) ($analytics['invalid_token'] ?? 0) + (int) ($analytics['token_expired'] ?? 0) + (int) ($analytics['token_revoked'] ?? 0) + (int) ($analytics['token_rate_limited'] ?? 0) + (int) ($analytics['event_not_public'] ?? 0) + (int) ($analytics['gallery_expired'] ?? 0); $lastSeenAt = $lastSeen->get($token->id); return [ 'id' => $token->id, 'label' => $token->label, 'token' => $token->token, 'url' => url('/e/'.$token->token), 'usage_limit' => $token->usage_limit, 'usage_count' => $token->usage_count, 'expires_at' => optional($token->expires_at)->toIso8601String(), 'revoked_at' => optional($token->revoked_at)->toIso8601String(), 'is_active' => $token->isActive(), 'created_at' => optional($token->created_at)->toIso8601String(), 'layouts' => $layouts, 'layouts_url' => route('api.v1.tenant.events.join-tokens.layouts.index', [ 'event' => $record->slug, 'joinToken' => $token->getKey(), ]), 'analytics' => [ 'success_total' => $successCount, 'failure_total' => $failureCount, 'rate_limited_total' => (int) ($analytics['token_rate_limited'] ?? 0), 'recent_24h' => (int) $recent24h->get($token->id, 0), 'last_seen_at' => $lastSeenAt ? Carbon::parse($lastSeenAt)->toIso8601String() : null, ], ]; }); return view('filament.events.join-link', [ 'event' => $record, 'tokens' => $tokens, ]); }), ]) ->bulkActions([ Actions\DeleteBulkAction::make() ->after(function (Collection $records): void { $logger = app(SuperAdminAuditLogger::class); foreach ($records as $record) { $logger->recordModelMutation( 'deleted', $record, source: static::class ); } }), ]); } public static function getRelations(): array { return [ EventPackagesRelationManager::class, ]; } /** * @param array|string|null $name */ private static function formatEventName(mixed $name): string { if (is_array($name)) { $candidates = [ $name['de'] ?? null, $name['en'] ?? null, reset($name) ?: null, ]; foreach ($candidates as $candidate) { if (is_string($candidate) && $candidate !== '') { return $candidate; } } return ''; } return is_string($name) ? $name : ''; } public static function getPages(): array { return [ 'index' => Pages\ListEvents::route('/'), 'create' => Pages\CreateEvent::route('/create'), 'view' => Pages\ViewEvent::route('/{record}'), 'edit' => Pages\EditEvent::route('/{record}/edit'), 'watermark' => Pages\ManageWatermark::route('/{record}/watermark'), ]; } }