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), DatePicker::make('date') ->label(__('admin.events.fields.date')) ->required(), Select::make('event_type_id') ->label(__('admin.events.fields.type')) ->options(EventType::query()->pluck('name', '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(), 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.de') ->label(__('admin.events.fields.name')) ->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('default_locale'), 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('primary_join_token') ->label(__('admin.events.table.join')) ->getStateUsing(function ($record) { $token = $record->joinTokens()->latest()->first(); return $token ? url('/e/' . $token->token) : __('admin.events.table.no_join_tokens'); }) ->description(function ($record) { $total = $record->joinTokens()->count(); return $total > 0 ? __('admin.events.table.join_tokens_total', ['count' => $total]) : __('admin.events.table.join_tokens_missing'); }) ->copyable() ->copyMessage(__('admin.events.messages.join_link_copied')), Tables\Columns\TextColumn::make('created_at')->since(), ]) ->filters([]) ->actions([ Actions\EditAction::make(), Actions\Action::make('toggle') ->label(__('admin.events.actions.toggle_active')) ->icon('heroicon-o-power') ->action(fn ($record) => $record->update(['is_active' => ! $record->is_active])), 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('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('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(), ]); } public static function getRelations(): array { return [ EventPackagesRelationManager::class, ]; } 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'), ]; } }