diff --git a/app/Filament/Tenant/Pages/InviteStudio.php b/app/Filament/Tenant/Pages/InviteStudio.php deleted file mode 100644 index 990af00..0000000 --- a/app/Filament/Tenant/Pages/InviteStudio.php +++ /dev/null @@ -1,187 +0,0 @@ -redirect(TenantOnboarding::getUrl()); - - return; - } - - $firstEventId = $tenant->events()->orderBy('date')->value('id'); - - $this->selectedEventId = $firstEventId; - $this->layouts = $this->buildLayouts(); - - if ($this->selectedEventId) { - $this->loadEventContext(); - } - } - - public static function shouldRegisterNavigation(): bool - { - return TenantOnboardingState::completed(); - } - - public function updatedSelectedEventId(): void - { - $this->loadEventContext(); - } - - public function createInvite(EventJoinTokenService $service): void - { - $this->validate([ - 'selectedEventId' => ['required', 'exists:events,id'], - 'tokenLabel' => ['nullable', 'string', 'max:120'], - ]); - - $tenant = TenantOnboardingState::tenant(); - - abort_if(! $tenant, 403); - - $event = $tenant->events()->whereKey($this->selectedEventId)->first(); - - if (! $event) { - Notification::make() - ->title('Event konnte nicht gefunden werden') - ->danger() - ->send(); - - return; - } - - $label = $this->tokenLabel ?: 'Einladung '.now()->format('d.m.'); - - $layoutPreference = Arr::get($tenant->settings ?? [], 'branding.preferred_invite_layout'); - - $service->createToken($event, [ - 'label' => $label, - 'metadata' => [ - 'preferred_layout' => $layoutPreference, - ], - 'created_by' => auth()->id(), - ]); - - $this->tokenLabel = ''; - - $this->loadEventContext(); - - Notification::make() - ->title('Neuer Einladungslink erstellt') - ->success() - ->send(); - } - - protected function loadEventContext(): void - { - $tenant = TenantOnboardingState::tenant(); - - if (! $tenant || ! $this->selectedEventId) { - $this->tokens = []; - - return; - } - - $event = $tenant->events()->whereKey($this->selectedEventId)->first(); - - if (! $event) { - $this->tokens = []; - - return; - } - - $this->tokens = $event->joinTokens() - ->orderByDesc('created_at') - ->get() - ->map(fn (EventJoinToken $token) => $this->mapToken($event, $token)) - ->toArray(); - } - - protected function mapToken(Event $event, EventJoinToken $token): array - { - $downloadUrls = JoinTokenLayoutRegistry::toResponse(function (string $layoutId, string $format) use ($event, $token) { - return route('api.v1.tenant.events.join-tokens.layouts.download', [ - 'event' => $event->slug, - 'joinToken' => $token->getKey(), - 'layout' => $layoutId, - 'format' => $format, - ]); - }); - - return [ - 'id' => $token->getKey(), - 'label' => $token->label ?? 'Einladungslink', - 'url' => URL::to('/e/'.$token->token), - 'created_at' => optional($token->created_at)->format('d.m.Y H:i'), - 'usage_count' => $token->usage_count, - 'usage_limit' => $token->usage_limit, - 'active' => $token->isActive(), - 'downloads' => $downloadUrls, - ]; - } - - protected function buildLayouts(): array - { - return collect(JoinTokenLayoutRegistry::all()) - ->map(fn (array $layout) => [ - 'id' => $layout['id'], - 'name' => $layout['name'], - 'subtitle' => $layout['subtitle'] ?? '', - 'description' => $layout['description'] ?? '', - ]) - ->toArray(); - } - - public function getEventsProperty(): Collection - { - $tenant = TenantOnboardingState::tenant(); - - if (! $tenant) { - return collect(); - } - - return $tenant->events()->orderBy('date')->get(); - } -} diff --git a/app/Filament/Tenant/Pages/TenantOnboarding.php b/app/Filament/Tenant/Pages/TenantOnboarding.php deleted file mode 100644 index e61b1c0..0000000 --- a/app/Filament/Tenant/Pages/TenantOnboarding.php +++ /dev/null @@ -1,311 +0,0 @@ -status = TenantOnboardingState::status($tenant); - - if (TenantOnboardingState::completed($tenant)) { - $this->redirect(EventResource::getUrl()); - - return; - } - - $this->eventDate = Carbon::now()->addWeeks(2)->format('Y-m-d'); - $this->eventTypeId = $this->getDefaultEventTypeId(); - } - - public static function shouldRegisterNavigation(): bool - { - $tenant = TenantOnboardingState::tenant(); - - return ! TenantOnboardingState::completed($tenant); - } - - public function start(): void - { - $this->step = 'packages'; - } - - public function savePackages(): void - { - $this->validate([ - 'selectedPackages' => ['required', 'array', 'min:1'], - 'selectedPackages.*' => ['integer', 'exists:task_collections,id'], - ], [ - 'selectedPackages.required' => 'Bitte wählt mindestens ein Aufgabenpaket aus.', - ]); - - $this->step = 'event'; - } - - public function saveEvent(): void - { - $this->validate([ - 'eventName' => ['required', 'string', 'max:255'], - 'eventDate' => ['required', 'date'], - 'eventTypeId' => ['required', 'exists:event_types,id'], - ]); - - $this->step = 'palette'; - } - - public function savePalette(): void - { - $this->validate([ - 'palette' => ['required', 'string'], - ]); - - $this->step = 'invite'; - } - - public function finish( - TaskCollectionImportService $importService, - EventJoinTokenService $joinTokenService - ): void { - $this->validate([ - 'inviteLayout' => ['required', 'string'], - ], [ - 'inviteLayout.required' => 'Bitte wählt ein Layout aus.', - ]); - - $tenant = TenantOnboardingState::tenant(); - - abort_if(! $tenant, 403); - - $this->isProcessing = true; - - try { - DB::transaction(function () use ($tenant, $importService, $joinTokenService) { - $event = $this->createEvent($tenant); - $this->importPackages($importService, $this->selectedPackages, $event); - - $token = $joinTokenService->createToken($event, [ - 'label' => 'Fotospiel Einladung', - 'metadata' => [ - 'preferred_layout' => $this->inviteLayout, - ], - ]); - - $settings = $tenant->settings ?? []; - Arr::set($settings, 'branding.palette', $this->palette); - Arr::set($settings, 'branding.primary_event_id', $event->id); - Arr::set($settings, 'branding.preferred_invite_layout', $this->inviteLayout); - $tenant->forceFill(['settings' => $settings])->save(); - - TenantOnboardingState::markCompleted($tenant, [ - 'primary_event_id' => $event->id, - 'selected_packages' => $this->selectedPackages, - 'qr_layout' => $this->inviteLayout, - ]); - - $this->inviteDownloads = $this->buildInviteDownloads($event, $token); - $this->status = TenantOnboardingState::status($tenant); - - Notification::make() - ->title('Euer Setup ist bereit!') - ->body('Wir haben euer Event erstellt, Aufgaben importiert und euren Einladungslink vorbereitet.') - ->success() - ->send(); - - $this->redirect(EventResource::getUrl('view', ['record' => $event])); - }); - } catch (Throwable $exception) { - report($exception); - - Notification::make() - ->title('Setup konnte nicht abgeschlossen werden') - ->body('Bitte prüft eure Eingaben oder versucht es später erneut.') - ->danger() - ->send(); - } finally { - $this->isProcessing = false; - } - } - - protected function createEvent($tenant): Event - { - $slugBase = Str::slug($this->eventName) ?: 'event'; - - do { - $slug = Str::of($slugBase)->append('-', Str::random(6))->lower(); - } while (Event::where('slug', $slug)->exists()); - - return Event::create([ - 'tenant_id' => $tenant->id, - 'name' => [ - app()->getLocale() => $this->eventName, - 'de' => $this->eventName, - ], - 'description' => null, - 'date' => $this->eventDate, - 'slug' => (string) $slug, - 'event_type_id' => $this->eventTypeId, - 'is_active' => true, - 'default_locale' => app()->getLocale(), - 'status' => 'draft', - 'settings' => [ - 'appearance' => [ - 'palette' => $this->palette, - ], - ], - ]); - } - - protected function importPackages( - TaskCollectionImportService $importService, - array $packageIds, - Event $event - ): void { - if (empty($packageIds)) { - return; - } - - /** @var EloquentCollection $collections */ - $collections = TaskCollection::query() - ->whereIn('id', $packageIds) - ->get(); - - $collections->each(function (TaskCollection $collection) use ($importService, $event) { - $importService->import($collection, $event); - }); - } - - protected function buildInviteDownloads(Event $event, $token): array - { - return JoinTokenLayoutRegistry::toResponse(function (string $layoutId, string $format) use ($event, $token) { - return route('api.v1.tenant.events.join-tokens.layouts.download', [ - 'event' => $event->slug, - 'joinToken' => $token->getKey(), - 'layout' => $layoutId, - 'format' => $format, - ]); - }); - } - - public function getPackageListProperty(): array - { - return TaskCollection::query() - ->whereNull('tenant_id') - ->orderBy('position') - ->get() - ->map(fn (TaskCollection $collection) => [ - 'id' => $collection->getKey(), - 'name' => $collection->name, - 'description' => $collection->description, - ]) - ->toArray(); - } - - public function getEventTypeOptionsProperty(): array - { - return EventType::query() - ->orderBy('name->'.app()->getLocale()) - ->get() - ->mapWithKeys(function (EventType $type) { - $name = $type->name[app()->getLocale()] ?? $type->name['de'] ?? Arr::first($type->name); - - return [$type->getKey() => $name]; - }) - ->toArray(); - } - - public function getPaletteOptionsProperty(): array - { - return [ - 'romance' => [ - 'label' => 'Rosé & Gold', - 'description' => 'Warme Rosé-Töne mit goldenen Akzenten – romantisch und elegant.', - ], - 'sunset' => [ - 'label' => 'Sonnenuntergang', - 'description' => 'Leuchtende Orange- und Pink-Verläufe für lebhafte Partys.', - ], - 'evergreen' => [ - 'label' => 'Evergreen', - 'description' => 'Sanfte Grüntöne und Naturakzente für Boho- & Outdoor-Events.', - ], - 'midnight' => [ - 'label' => 'Midnight', - 'description' => 'Tiefes Navy und Flieder – perfekt für elegante Abendveranstaltungen.', - ], - ]; - } - - public function getLayoutOptionsProperty(): array - { - return collect(JoinTokenLayoutRegistry::all()) - ->map(fn ($layout) => [ - 'id' => $layout['id'], - 'name' => $layout['name'], - 'subtitle' => $layout['subtitle'] ?? '', - 'description' => $layout['description'] ?? '', - ]) - ->toArray(); - } - - protected function getDefaultEventTypeId(): ?int - { - return EventType::query()->orderBy('name->'.app()->getLocale())->value('id'); - } -} diff --git a/app/Filament/Tenant/Resources/EventResource.php b/app/Filament/Tenant/Resources/EventResource.php deleted file mode 100644 index 73135c7..0000000 --- a/app/Filament/Tenant/Resources/EventResource.php +++ /dev/null @@ -1,264 +0,0 @@ -tenant_id; - - return $form->schema([ - Hidden::make('tenant_id') - ->default($tenantId) - ->dehydrated(), - TextInput::make('name') - ->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::all()->pluck('name', 'id')) - ->searchable(), - Select::make('package_id') - ->label(__('admin.events.fields.package')) - ->options(\App\Models\Package::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')), - ])->columns(2); - } - - public static function table(Table $table): Table - { - return $table - ->columns([ - Tables\Columns\TextColumn::make('id')->sortable(), - Tables\Columns\TextColumn::make('eventPackage.package.name') - ->label(__('admin.events.table.package')) - ->badge() - ->color('success'), - Tables\Columns\TextColumn::make('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.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()->orderByDesc('created_at')->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(), - ]) - ->modifyQueryUsing(function (Builder $query) { - if ($tenantId = Auth::user()?->tenant_id) { - $query->where('tenant_id', $tenantId); - } - }) - ->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('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(), - ]); - } - - 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'), - ]; - } - - public static function getRelations(): array - { - return [ - EventPackagesRelationManager::class, - ]; - } -} diff --git a/app/Filament/Tenant/Resources/EventResource/Pages/CreateEvent.php b/app/Filament/Tenant/Resources/EventResource/Pages/CreateEvent.php deleted file mode 100644 index c27d96b..0000000 --- a/app/Filament/Tenant/Resources/EventResource/Pages/CreateEvent.php +++ /dev/null @@ -1,11 +0,0 @@ -schema([ - Select::make('package_id') - ->label('Package') - ->relationship('package', 'name') - ->searchable() - ->preload() - ->required(), - TextInput::make('purchased_price') - ->label('Kaufpreis') - ->prefix('€') - ->numeric() - ->step(0.01) - ->required(), - TextInput::make('used_photos') - ->label('Verwendete Fotos') - ->numeric() - ->default(0) - ->readOnly(), - TextInput::make('used_guests') - ->label('Verwendete Gäste') - ->numeric() - ->default(0) - ->readOnly(), - ]); - } - - public function table(Table $table): Table - { - return $table - ->recordTitleAttribute('package.name') - ->columns([ - TextColumn::make('package.name') - ->label('Package') - ->badge() - ->color('success'), - TextColumn::make('used_photos') - ->label('Verwendete Fotos') - ->badge(), - TextColumn::make('remaining_photos') - ->label('Verbleibende Fotos') - ->badge() - ->color(fn ($state) => $state < 1 ? 'danger' : 'success') - ->getStateUsing(fn (EventPackage $record) => $record->remaining_photos), - TextColumn::make('used_guests') - ->label('Verwendete Gäste') - ->badge(), - TextColumn::make('remaining_guests') - ->label('Verbleibende Gäste') - ->badge() - ->color(fn ($state) => $state < 1 ? 'danger' : 'success') - ->getStateUsing(fn (EventPackage $record) => $record->remaining_guests), - TextColumn::make('expires_at') - ->label('Ablauf') - ->dateTime() - ->badge() - ->color(fn ($state) => $state && $state->isPast() ? 'danger' : 'success'), - TextColumn::make('price') - ->label('Preis') - ->money('EUR') - ->sortable(), - ]) - ->filters([ - // - ]) - ->headerActions([ - CreateAction::make(), - ]) - ->actions([ - EditAction::make(), - DeleteAction::make(), - ]) - ->bulkActions([ - BulkActionGroup::make([ - DeleteBulkAction::make(), - ]), - ]); - } - - public function getRelationExistenceQuery( - Builder $query, - string $relationshipName, - ?string $ownerKeyName, - mixed $ownerKeyValue, - ): Builder { - return $query; - } - - public static function getTitle(Model $ownerRecord, string $pageClass): string - { - return __('admin.events.relation_managers.event_packages.title'); - } - - public function getTableQuery(): Builder | Relation - { - return parent::getTableQuery() - ->with('package'); - } -} diff --git a/app/Filament/Tenant/Resources/PhotoResource.php b/app/Filament/Tenant/Resources/PhotoResource.php deleted file mode 100644 index d96d1b8..0000000 --- a/app/Filament/Tenant/Resources/PhotoResource.php +++ /dev/null @@ -1,126 +0,0 @@ -tenant_id; - - return $form->schema([ - Select::make('event_id') - ->label(__('admin.photos.fields.event')) - ->options( - Event::query() - ->when($tenantId, fn ($query) => $query->where('tenant_id', $tenantId)) - ->pluck('name', 'id') - ) - ->searchable() - ->required(), - FileUpload::make('file_path') - ->label(__('admin.photos.fields.photo')) - ->image() // enable FilePond image preview - ->disk('public') - ->directory('photos') - ->visibility('public') - ->required(), - Toggle::make('is_featured') - ->label(__('admin.photos.fields.is_featured')) - ->default(false), - KeyValue::make('metadata') - ->label(__('admin.photos.fields.metadata')) - ->keyLabel(__('admin.common.key')) - ->valueLabel(__('admin.common.value')), - ])->columns(2); - } - - public static function table(Table $table): Table - { - return $table - ->columns([ - Tables\Columns\ImageColumn::make('file_path')->label(__('admin.photos.table.photo'))->disk('public')->visibility('public'), - Tables\Columns\TextColumn::make('id')->sortable(), - Tables\Columns\TextColumn::make('event.name')->label(__('admin.photos.table.event'))->searchable(), - Tables\Columns\TextColumn::make('likes_count')->label(__('admin.photos.table.likes')), - Tables\Columns\IconColumn::make('is_featured')->boolean(), - Tables\Columns\TextColumn::make('created_at')->since(), - ]) - ->modifyQueryUsing(function (Builder $query) { - if ($tenantId = Auth::user()?->tenant_id) { - $query->whereHas('event', fn (Builder $eventQuery) => $eventQuery->where('tenant_id', $tenantId)); - } - }) - ->filters([]) - ->actions([ - Actions\EditAction::make(), - Actions\Action::make('feature') - ->label(__('admin.photos.actions.feature')) - ->visible(fn($record) => !$record->is_featured) - ->action(fn($record) => $record->update(['is_featured' => true])) - ->icon('heroicon-o-star'), - Actions\Action::make('unfeature') - ->label(__('admin.photos.actions.unfeature')) - ->visible(fn($record) => $record->is_featured) - ->action(fn($record) => $record->update(['is_featured' => false])) - ->icon('heroicon-o-star'), - Actions\DeleteAction::make(), - ]) - ->bulkActions([ - Actions\BulkAction::make('feature') - ->label(__('admin.photos.actions.feature_selected')) - ->icon('heroicon-o-star') - ->action(fn($records) => $records->each->update(['is_featured' => true])), - Actions\BulkAction::make('unfeature') - ->label(__('admin.photos.actions.unfeature_selected')) - ->icon('heroicon-o-star') - ->action(fn($records) => $records->each->update(['is_featured' => false])), - Actions\DeleteBulkAction::make(), - ]); - } - - public static function getPages(): array - { - return [ - 'index' => Pages\ListPhotos::route('/'), - 'view' => Pages\ViewPhoto::route('/{record}'), - 'edit' => Pages\EditPhoto::route('/{record}/edit'), - ]; - } -} diff --git a/app/Filament/Tenant/Resources/PhotoResource/Pages/EditPhoto.php b/app/Filament/Tenant/Resources/PhotoResource/Pages/EditPhoto.php deleted file mode 100644 index 5f2da5b..0000000 --- a/app/Filament/Tenant/Resources/PhotoResource/Pages/EditPhoto.php +++ /dev/null @@ -1,11 +0,0 @@ -user()?->tenant_id; - - return $schema->components([ - Section::make(__('Task Collection Details')) - ->schema([ - TextInput::make('name_translations.de') - ->label(__('Name (DE)')) - ->required() - ->maxLength(255) - ->disabled(fn (?TaskCollection $record) => $record?->tenant_id !== $tenantId && $record !== null), - TextInput::make('name_translations.en') - ->label(__('Name (EN)')) - ->maxLength(255) - ->disabled(fn (?TaskCollection $record) => $record?->tenant_id !== $tenantId && $record !== null), - Select::make('event_type_id') - ->label(__('Event Type')) - ->options(fn () => EventType::orderBy('name->' . app()->getLocale()) - ->get() - ->mapWithKeys(function (EventType $type) { - $name = $type->name[app()->getLocale()] ?? $type->name['de'] ?? reset($type->name); - - return [$type->id => $name]; - })->toArray()) - ->searchable() - ->required() - ->disabled(fn (?TaskCollection $record) => $record?->tenant_id !== $tenantId && $record !== null), - Textarea::make('description_translations.de') - ->label(__('Description (DE)')) - ->rows(3) - ->disabled(fn (?TaskCollection $record) => $record?->tenant_id !== $tenantId && $record !== null), - Textarea::make('description_translations.en') - ->label(__('Description (EN)')) - ->rows(3) - ->disabled(fn (?TaskCollection $record) => $record?->tenant_id !== $tenantId && $record !== null), - ])->columns(2), - ]); - } - - public static function table(Table $table): Table - { - return $table - ->columns([ - TextColumn::make('name') - ->label(__('Name')) - ->searchable(['name_translations->de', 'name_translations->en']) - ->sortable(), - BadgeColumn::make('eventType.name') - ->label(__('Event Type')) - ->color('info'), - IconColumn::make('tenant_id') - ->label(__('Scope')) - ->boolean() - ->trueIcon('heroicon-o-user-group') - ->falseIcon('heroicon-o-globe-alt') - ->state(fn (TaskCollection $record) => $record->tenant_id !== null) - ->tooltip(fn (TaskCollection $record) => $record->tenant_id ? __('Tenant-only') : __('Global template')), - TextColumn::make('tasks_count') - ->label(__('Tasks')) - ->counts('tasks') - ->sortable(), - ]) - ->filters([ - SelectFilter::make('event_type_id') - ->label(__('Event Type')) - ->relationship('eventType', 'name->' . app()->getLocale()), - SelectFilter::make('scope') - ->options([ - 'global' => __('Global template'), - 'tenant' => __('Tenant-owned'), - ]) - ->query(function ($query, $value) { - $tenantId = auth()->user()?->tenant_id; - - if ($value === 'global') { - $query->whereNull('tenant_id'); - } - - if ($value === 'tenant') { - $query->where('tenant_id', $tenantId); - } - }), - ]) - ->actions([ - \Filament\Actions\Action::make('import') - ->label(__('Import to Event')) - ->icon('heroicon-o-cloud-arrow-down') - ->form([ - Select::make('event_slug') - ->label(__('Select Event')) - ->options(function () { - $tenantId = auth()->user()?->tenant_id; - - return Event::where('tenant_id', $tenantId) - ->orderBy('date', 'desc') - ->get() - ->mapWithKeys(function (Event $event) { - $name = $event->name[app()->getLocale()] ?? $event->name['de'] ?? reset($event->name); - - return [ - $event->slug => sprintf('%s (%s)', $name, $event->date?->format('d.m.Y')), - ]; - })->toArray(); - }) - ->required() - ->searchable(), - ]) - ->action(function (TaskCollection $record, array $data) { - $event = Event::where('slug', $data['event_slug']) - ->where('tenant_id', auth()->user()?->tenant_id) - ->firstOrFail(); - - /** @var TaskCollectionImportService $service */ - $service = app(TaskCollectionImportService::class); - $service->import($record, $event); - - Notification::make() - ->title(__('Task collection imported')) - ->body(__('The collection :name has been imported.', ['name' => $record->name])) - ->success() - ->send(); - }), - Actions\EditAction::make() - ->label(__('Edit')) - ->visible(fn (TaskCollection $record) => $record->tenant_id === auth()->user()?->tenant_id), - ]) - ->headerActions([ - Actions\CreateAction::make() - ->label(__('Create Task Collection')) - ->mutateFormDataUsing(function (array $data) { - $tenantId = auth()->user()?->tenant_id; - - $data['tenant_id'] = $tenantId; - $data['slug'] = static::generateSlug($data['name_translations']['en'] ?? $data['name_translations']['de'] ?? 'collection', $tenantId); - - return $data; - }), - ]) - ->bulkActions([ - Actions\DeleteBulkAction::make() - ->visible(fn () => false), - ]); - } - - public static function getPages(): array - { - return [ - 'index' => Pages\ListTaskCollections::route('/'), - 'create' => Pages\CreateTaskCollection::route('/create'), - 'edit' => Pages\EditTaskCollection::route('/{record}/edit'), - ]; - } - - public static function getEloquentQuery(): Builder - { - $tenantId = auth()->user()?->tenant_id; - - return parent::getEloquentQuery() - ->forTenant($tenantId) - ->with('eventType') - ->withCount('tasks'); - } - - public static function getGloballySearchableAttributes(): array - { - return ['name_translations->de', 'name_translations->en']; - } - - public static function generateSlug(string $base, int $tenantId): string - { - $slugBase = Str::slug($base) ?: 'collection'; - - do { - $candidate = $slugBase . '-' . $tenantId . '-' . Str::random(4); - } while (TaskCollection::where('slug', $candidate)->exists()); - - return $candidate; - } - - public static function scopeEloquentQueryToTenant(Builder $query, ?Model $tenant): Builder - { - $tenant ??= Filament::getTenant(); - - if (! $tenant) { - return $query; - } - - return $query->where(function (Builder $innerQuery) use ($tenant) { - $innerQuery->whereNull('tenant_id') - ->orWhere('tenant_id', $tenant->getKey()); - }); - } -} diff --git a/app/Filament/Tenant/Resources/TaskCollectionResource/Pages/CreateTaskCollection.php b/app/Filament/Tenant/Resources/TaskCollectionResource/Pages/CreateTaskCollection.php deleted file mode 100644 index e537467..0000000 --- a/app/Filament/Tenant/Resources/TaskCollectionResource/Pages/CreateTaskCollection.php +++ /dev/null @@ -1,26 +0,0 @@ -tenant_id; - - $data['tenant_id'] = $tenantId; - $data['slug'] = TaskCollectionResource::generateSlug( - $data['name_translations']['en'] ?? $data['name_translations']['de'] ?? 'collection', - $tenantId - ); - - return $data; - } -} diff --git a/app/Filament/Tenant/Resources/TaskCollectionResource/Pages/EditTaskCollection.php b/app/Filament/Tenant/Resources/TaskCollectionResource/Pages/EditTaskCollection.php deleted file mode 100644 index 92b29da..0000000 --- a/app/Filament/Tenant/Resources/TaskCollectionResource/Pages/EditTaskCollection.php +++ /dev/null @@ -1,23 +0,0 @@ -getRecord(); - - if ($record->tenant_id !== Auth::user()?->tenant_id) { - abort(403); - } - } -} diff --git a/app/Filament/Tenant/Resources/TaskCollectionResource/Pages/ListTaskCollections.php b/app/Filament/Tenant/Resources/TaskCollectionResource/Pages/ListTaskCollections.php deleted file mode 100644 index b65f77e..0000000 --- a/app/Filament/Tenant/Resources/TaskCollectionResource/Pages/ListTaskCollections.php +++ /dev/null @@ -1,11 +0,0 @@ -tenant_id; - - return $form->schema([ - Select::make('emotion_id') - ->relationship('emotion', 'name') - ->required() - ->searchable() - ->preload(), - Select::make('event_type_id') - ->relationship('eventType', 'name') - ->searchable() - ->preload() - ->label(__('admin.tasks.fields.event_type_optional')), - SchemaTabs::make('content_tabs') - ->label(__('admin.tasks.fields.content_localization')) - ->tabs([ - SchemaTab::make(__('admin.common.german')) - ->icon('heroicon-o-language') - ->schema([ - TextInput::make('title.de') - ->label(__('admin.tasks.fields.title_de')) - ->required(), - MarkdownEditor::make('description.de') - ->label(__('admin.tasks.fields.description_de')) - ->columnSpanFull(), - MarkdownEditor::make('example_text.de') - ->label(__('admin.tasks.fields.example_de')) - ->columnSpanFull(), - ]), - SchemaTab::make(__('admin.common.english')) - ->icon('heroicon-o-language') - ->schema([ - TextInput::make('title.en') - ->label(__('admin.tasks.fields.title_en')) - ->required(), - MarkdownEditor::make('description.en') - ->label(__('admin.tasks.fields.description_en')) - ->columnSpanFull(), - MarkdownEditor::make('example_text.en') - ->label(__('admin.tasks.fields.example_en')) - ->columnSpanFull(), - ]), - ]) - ->columnSpanFull(), - Select::make('difficulty') - ->label(__('admin.tasks.fields.difficulty.label')) - ->options([ - 'easy' => __('admin.tasks.fields.difficulty.easy'), - 'medium' => __('admin.tasks.fields.difficulty.medium'), - 'hard' => __('admin.tasks.fields.difficulty.hard'), - ]) - ->default('easy'), - TextInput::make('sort_order') - ->numeric() - ->default(0), - Toggle::make('is_active') - ->default(true), - Select::make('assigned_events') - ->label(__('admin.tasks.fields.events')) - ->multiple() - ->relationship( - 'assignedEvents', - 'name', - fn (Builder $query) => $tenantId - ? $query->where('tenant_id', $tenantId) - : $query - ) - ->searchable() - ->preload() - ->getOptionLabelFromRecordUsing(fn (Event $record) => $record->name) - ->helperText(__('admin.tasks.fields.events_helper')), - ])->columns(2); - } - - public static function table(Table $table): Table - { - $tenantId = Auth::user()?->tenant_id; - - return $table - ->columns([ - Tables\Columns\TextColumn::make('id') - ->label('#') - ->sortable(), - Tables\Columns\TextColumn::make('title') - ->label(__('admin.tasks.table.title')) - ->getStateUsing(function ($record) { - $value = $record->title; - if (is_array($value)) { - $loc = app()->getLocale(); - return $value[$loc] ?? ($value['de'] ?? ($value['en'] ?? '')); - } - - return (string) $value; - }) - ->limit(60) - ->searchable(['title->de', 'title->en']), - Tables\Columns\TextColumn::make('emotion.name') - ->label(__('admin.tasks.fields.emotion')) - ->toggleable(), - Tables\Columns\TextColumn::make('eventType.name') - ->label(__('admin.tasks.fields.event_type')) - ->toggleable(), - Tables\Columns\TextColumn::make('assignedEvents.name') - ->label(__('admin.tasks.table.events')) - ->badge() - ->separator(', ') - ->limitList(2), - Tables\Columns\TextColumn::make('difficulty') - ->label(__('admin.tasks.fields.difficulty.label')) - ->badge(), - Tables\Columns\IconColumn::make('is_active') - ->label(__('admin.tasks.table.is_active')) - ->boolean(), - Tables\Columns\TextColumn::make('sort_order') - ->label(__('admin.tasks.table.sort_order')) - ->sortable(), - ]) - ->filters([]) - ->actions([ - Actions\EditAction::make(), - Actions\DeleteAction::make(), - ]) - ->bulkActions([ - Actions\DeleteBulkAction::make(), - ]) - ->modifyQueryUsing(function (Builder $query) use ($tenantId) { - if (! $tenantId) { - return $query; - } - - $query->forTenant($tenantId); - - return $query; - }); - } - - public static function getPages(): array - { - return [ - 'index' => Pages\ListTasks::route('/'), - 'create' => Pages\CreateTask::route('/create'), - 'edit' => Pages\EditTask::route('/{record}/edit'), - ]; - } - - public static function scopeEloquentQueryToTenant(Builder $query, ?Model $tenant): Builder - { - $tenant ??= Filament::getTenant(); - - if (! $tenant) { - return $query; - } - - return $query->where(function (Builder $innerQuery) use ($tenant) { - $innerQuery->whereNull('tenant_id') - ->orWhere('tenant_id', $tenant->getKey()); - }); - } -} diff --git a/app/Filament/Tenant/Resources/TaskResource/Pages/CreateTask.php b/app/Filament/Tenant/Resources/TaskResource/Pages/CreateTask.php deleted file mode 100644 index 0b06513..0000000 --- a/app/Filament/Tenant/Resources/TaskResource/Pages/CreateTask.php +++ /dev/null @@ -1,11 +0,0 @@ -toDateString(); - return $table - ->query( - Event::query() - ->where('is_active', true) - ->whereDate('date', '<=', $today) - ->withCount([ - 'photos as uploads_today' => function ($q) use ($today) { - $q->whereDate('created_at', $today); - }, - ]) - ->orderByDesc('date') - ->limit(10) - ) - ->columns([ - Tables\Columns\TextColumn::make('id')->label(__('admin.common.hash'))->width('60px'), - Tables\Columns\TextColumn::make('slug')->label(__('admin.common.slug'))->searchable(), - Tables\Columns\TextColumn::make('date')->date(), - Tables\Columns\TextColumn::make('uploads_today')->label(__('admin.common.uploads_today'))->numeric(), - ]) - ->paginated(false); - } -} - - diff --git a/app/Filament/Tenant/Widgets/RecentPhotosTable.php b/app/Filament/Tenant/Widgets/RecentPhotosTable.php deleted file mode 100644 index f0d886d..0000000 --- a/app/Filament/Tenant/Widgets/RecentPhotosTable.php +++ /dev/null @@ -1,48 +0,0 @@ -query( - Photo::query() - ->orderByDesc('created_at') - ->limit(10) - ) - ->columns([ - Tables\Columns\ImageColumn::make('thumbnail_path')->label(__('admin.common.thumb'))->circular(), - Tables\Columns\TextColumn::make('id')->label(__('admin.common.hash')), - Tables\Columns\TextColumn::make('event_id')->label(__('admin.common.event')), - Tables\Columns\TextColumn::make('likes_count')->label(__('admin.common.likes')), - Tables\Columns\TextColumn::make('created_at')->since(), - ]) - ->actions([ - Actions\Action::make('feature') - ->label(__('admin.photos.actions.feature')) - ->visible(fn(Photo $record) => ! (bool)($record->is_featured ?? 0)) - ->action(fn(Photo $record) => $record->update(['is_featured' => 1])), - Actions\Action::make('unfeature') - ->label(__('admin.photos.actions.unfeature')) - ->visible(fn(Photo $record) => (bool)($record->is_featured ?? 0)) - ->action(fn(Photo $record) => $record->update(['is_featured' => 0])), - ]) - ->paginated(false); - } -} - diff --git a/app/Filament/Tenant/Widgets/UploadsPerDayChart.php b/app/Filament/Tenant/Widgets/UploadsPerDayChart.php deleted file mode 100644 index b3aa55b..0000000 --- a/app/Filament/Tenant/Widgets/UploadsPerDayChart.php +++ /dev/null @@ -1,56 +0,0 @@ -startOfDay()->subDays(13); - for ($i = 0; $i < 14; $i++) { - $labels[] = $start->copy()->addDays($i)->format('Y-m-d'); - } - - // SQLite-friendly group by date - $rows = DB::table('photos') - ->selectRaw("strftime('%Y-%m-%d', created_at) as d, count(*) as c") - ->where('created_at', '>=', $start) - ->groupBy('d') - ->orderBy('d') - ->get(); - $map = collect($rows)->keyBy('d'); - $data = array_map(fn ($d) => (int) ($map[$d]->c ?? 0), $labels); - - return [ - 'labels' => $labels, - 'datasets' => [ - [ - 'label' => __('admin.common.uploads'), - 'data' => $data, - 'borderColor' => '#f59e0b', - 'backgroundColor' => 'rgba(245, 158, 11, 0.2)', - 'tension' => 0.3, - ], - ], - ]; - } - - protected function getType(): string - { - return 'line'; - } - - public function getHeading(): string|\Illuminate\Contracts\Support\Htmlable|null - { - return __('admin.widgets.uploads_per_day.heading'); - } -} diff --git a/app/Http/Controllers/Auth/AuthenticatedSessionController.php b/app/Http/Controllers/Auth/AuthenticatedSessionController.php index a9622b7..abc82fe 100644 --- a/app/Http/Controllers/Auth/AuthenticatedSessionController.php +++ b/app/Http/Controllers/Auth/AuthenticatedSessionController.php @@ -156,7 +156,7 @@ class AuthenticatedSessionController extends Controller // Super admins go to Filament superadmin panel if ($user && $user->role === 'super_admin') { - return '/admin'; + return '/super-admin'; } // Tenant admins go to their PWA dashboard diff --git a/app/Http/Middleware/RedirectIfAuthenticated.php b/app/Http/Middleware/RedirectIfAuthenticated.php index dda0c44..fa0d496 100644 --- a/app/Http/Middleware/RedirectIfAuthenticated.php +++ b/app/Http/Middleware/RedirectIfAuthenticated.php @@ -113,7 +113,7 @@ class RedirectIfAuthenticated extends BaseMiddleware } if ($user && $user->role === 'super_admin') { - return '/admin'; + return '/super-admin'; } if ($user && $user->role === 'user') { diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index f93b715..afa8356 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -301,8 +301,5 @@ class AppServiceProvider extends ServiceProvider ]); }); - if ($this->app->runningInConsole()) { - $this->app->register(\App\Providers\Filament\AdminPanelProvider::class); - } } } diff --git a/app/Providers/Filament/AdminPanelProvider.php b/app/Providers/Filament/AdminPanelProvider.php deleted file mode 100644 index 86eaa0f..0000000 --- a/app/Providers/Filament/AdminPanelProvider.php +++ /dev/null @@ -1,59 +0,0 @@ -id('admin') - ->path('admin') - ->brandName('Fotospiel Studio') - ->login(\App\Filament\Pages\Auth\Login::class) - ->colors([ - 'primary' => Color::Pink, - ]) - ->homeUrl(fn () => \App\Filament\Tenant\Pages\TenantOnboarding::getUrl()) - ->discoverResources(in: app_path('Filament/Tenant/Resources'), for: 'App\\Filament\\Tenant\\Resources') - ->discoverPages(in: app_path('Filament/Tenant/Pages'), for: 'App\\Filament\\Tenant\\Pages') - ->pages([]) - ->discoverWidgets(in: app_path('Filament/Tenant/Widgets'), for: 'App\\Filament\\Tenant\\Widgets') - ->widgets([ - Widgets\AccountWidget::class, - ]) - ->middleware([ - EncryptCookies::class, - AddQueuedCookiesToResponse::class, - StartSession::class, - AuthenticateSession::class, - ShareErrorsFromSession::class, - VerifyCsrfToken::class, - SubstituteBindings::class, - DisableBladeIconComponents::class, - DispatchServingFilamentEvent::class, - ]) - ->authMiddleware([ - Authenticate::class, - ]) - ->tenant(\App\Models\Tenant::class) - // Remove blog models as they are global and handled in SuperAdmin - ; - } -} diff --git a/resources/views/filament/tenant/pages/invite-studio.blade.php b/resources/views/filament/tenant/pages/invite-studio.blade.php deleted file mode 100644 index 781d883..0000000 --- a/resources/views/filament/tenant/pages/invite-studio.blade.php +++ /dev/null @@ -1,122 +0,0 @@ - -
-
-
-

Einladungen & QR-Codes

-

- Erstellt und verwaltet eure QR-Einladungen. Jeder Link enthält druckfertige Layouts als PDF & SVG. -

-
- -
-
- -
-
-

Tipp

-

Druckt mehrere Layouts aus und verteilt sie am Eingang, am Gästebuch und beim DJ-Pult.

-
-
-
- -
-
- -
- -
-
- @if ($this->events->isEmpty()) -

Legt zunächst ein Event an, um Einladungslinks zu erstellen.

- @endif -
- -
-

Aktive Einladungslinks

- - @if (empty($tokens)) -

- Noch keine Einladungen erstellt. Generiert euren ersten Link, um die QR-Codes als PDF oder SVG herunterzuladen. -

- @else -
- @foreach ($tokens as $token) -
-
-
-

{{ $token['label'] }}

-

- {{ $token['url'] }} · erstellt am {{ $token['created_at'] }} · Aufrufe: {{ $token['usage_count'] }}{{ $token['usage_limit'] ? ' / ' . $token['usage_limit'] : '' }} -

-
-
- - Link öffnen - - -
-
- -
- @foreach ($token['downloads'] as $layout) -
-

{{ $layout['name'] }}

-

{{ $layout['subtitle'] }}

-
- @foreach ($layout['download_urls'] as $format => $url) - - {{ strtoupper($format) }} herunterladen - - @endforeach -
-
- @endforeach -
-
- @endforeach -
- @endif -
-
-
diff --git a/resources/views/filament/tenant/pages/onboarding.blade.php b/resources/views/filament/tenant/pages/onboarding.blade.php deleted file mode 100644 index c4877f4..0000000 --- a/resources/views/filament/tenant/pages/onboarding.blade.php +++ /dev/null @@ -1,278 +0,0 @@ -@php - $steps = [ - 'intro' => 'Willkommen', - 'packages' => 'Aufgaben wählen', - 'event' => 'Event benennen', - 'palette' => 'Farbwelt', - 'invite' => 'Einladungen', - ]; -@endphp - - -
-
-
-

- Fotospiel Studio · Geführter Start -

-

- Eure Gäste werden zu Geschichtenerzähler:innen -

-

- Wir richten mit euch die Aufgaben, das Event und den ersten QR-Code ein. Alles ohne Technikstress – Schritt für Schritt. -

-
- -
    - @foreach ($steps as $key => $label) -
  1. - $step === $key, - 'bg-white text-rose-500 border border-rose-200' => $step !== $key, - ])> - {{ $loop->iteration }} - - - {{ $label }} - -
  2. - @endforeach -
-
- - @if ($step === 'intro') -
-
-

So funktioniert die Fotospiel App

-
-
-

1 · Aufgaben auswählen

-

Kuratiere Aufgaben, die eure Gäste inspirieren – ohne schon Fotos zu sammeln.

-
-
-

2 · Event gestalten

-

Name, Datum und Farben bestimmen das Look & Feel eurer Fotostory.

-
-
-

3 · QR-Code teilen

-

Euer Einladungslink führt Gäste direkt in die Galerie – kein App-Download notwendig.

-
-
- -
-
- @endif - - @if ($step === 'packages') -
-
-
-

Wählt euer erstes Aufgabenpaket

-

Jedes Paket enthält spielerische Prompts. Ihr könnt später weitere hinzufügen oder eigene Aufgaben erstellen.

-
-
- @foreach ($this->packageList as $package) - - @endforeach -
- @error('selectedPackages') -

{{ $message }}

- @enderror -
- -
-
-
- @endif - - @if ($step === 'event') -
-
-
-

Wie heißt euer Event?

-

Name und Anlass erscheinen in eurer Gästegalerie und auf den Einladungen.

-
-
-
- - @error('eventName') -

{{ $message }}

- @enderror -
-
- - @error('eventDate') -

{{ $message }}

- @enderror -
-
- - @error('eventTypeId') -

{{ $message }}

- @enderror -
-
-
- - -
-
-
- @endif - - @if ($step === 'palette') -
-
-
-

Welche Farben spiegeln eure Story?

-

Wir wenden die Palette auf Karten, QR-Layouts und App-Elemente an. Ihr könnt sie später jederzeit ändern.

-
-
- @foreach ($this->paletteOptions as $value => $data) - - @endforeach -
- @error('palette') -

{{ $message }}

- @enderror -
- - -
-
-
- @endif - - @if ($step === 'invite') -
-
-
-

Wie soll euer Einladungs-Layout aussehen?

-

Wir generieren sofort einen QR-Code samt PDF/SVG-Downloads.

-
-
- @foreach ($this->layoutOptions as $layout) - - @endforeach -
- @error('inviteLayout') -

{{ $message }}

- @enderror -
- - -
-
-
- @endif -
-
diff --git a/tests/Feature/Auth/RoleBasedLoginTest.php b/tests/Feature/Auth/RoleBasedLoginTest.php index 0099a08..251320a 100644 --- a/tests/Feature/Auth/RoleBasedLoginTest.php +++ b/tests/Feature/Auth/RoleBasedLoginTest.php @@ -30,7 +30,7 @@ class RoleBasedLoginTest extends TestCase $this->assertEquals('tenant@example.com', Auth::user()->email); } - public function test_super_admin_redirects_to_admin_panel() + public function test_super_admin_redirects_to_super_admin_panel() { $user = User::factory()->create([ 'email' => 'super@example.com', @@ -45,7 +45,7 @@ class RoleBasedLoginTest extends TestCase ]); $this->assertAuthenticated(); - $response->assertRedirect('/admin'); + $response->assertRedirect('/super-admin'); $this->assertEquals('super@example.com', Auth::user()->email); }