tenant admin startseite schicker gestaltet und super-admin und tenant admin (filament) aufgesplittet.
Es gibt nun task collections und vordefinierte tasks für alle. Onboarding verfeinert und webseite-carousel gefixt (logging später entfernen!)
This commit is contained in:
186
app/Filament/Tenant/Pages/InviteStudio.php
Normal file
186
app/Filament/Tenant/Pages/InviteStudio.php
Normal file
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Tenant\Pages;
|
||||
use App\Models\Event;
|
||||
use App\Models\EventJoinToken;
|
||||
use App\Services\EventJoinTokenService;
|
||||
use App\Support\JoinTokenLayoutRegistry;
|
||||
use App\Support\TenantOnboardingState;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Pages\Page;
|
||||
use BackedEnum;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
|
||||
class InviteStudio extends Page
|
||||
{
|
||||
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-qr-code';
|
||||
|
||||
protected string $view = 'filament.tenant.pages.invite-studio';
|
||||
|
||||
protected static ?string $navigationLabel = 'Einladungen & QR';
|
||||
|
||||
protected static ?string $slug = 'invite-studio';
|
||||
|
||||
protected static ?string $title = 'Einladungen & QR-Codes';
|
||||
|
||||
protected static ?int $navigationSort = 50;
|
||||
|
||||
public ?int $selectedEventId = null;
|
||||
|
||||
public string $tokenLabel = '';
|
||||
|
||||
public array $tokens = [];
|
||||
|
||||
public array $layouts = [];
|
||||
|
||||
protected static bool $shouldRegisterNavigation = true;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$tenant = TenantOnboardingState::tenant();
|
||||
|
||||
abort_if(! $tenant, 403);
|
||||
|
||||
if (! TenantOnboardingState::completed($tenant)) {
|
||||
$this->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('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();
|
||||
}
|
||||
}
|
||||
311
app/Filament/Tenant/Pages/TenantOnboarding.php
Normal file
311
app/Filament/Tenant/Pages/TenantOnboarding.php
Normal file
@@ -0,0 +1,311 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Tenant\Pages;
|
||||
|
||||
use App\Filament\Tenant\Resources\EventResource;
|
||||
use App\Models\Event;
|
||||
use App\Models\EventType;
|
||||
use App\Models\TaskCollection;
|
||||
use App\Services\EventJoinTokenService;
|
||||
use App\Services\Tenant\TaskCollectionImportService;
|
||||
use App\Support\JoinTokenLayoutRegistry;
|
||||
use App\Support\TenantOnboardingState;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Pages\Page;
|
||||
use BackedEnum;
|
||||
use UnitEnum;
|
||||
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Throwable;
|
||||
|
||||
class TenantOnboarding extends Page
|
||||
{
|
||||
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-sparkles';
|
||||
|
||||
protected string $view = 'filament.tenant.pages.onboarding';
|
||||
|
||||
protected static ?string $navigationLabel = 'Willkommen';
|
||||
|
||||
protected static ?string $slug = 'willkommen';
|
||||
|
||||
protected static ?string $title = 'Euer Start mit Fotospiel';
|
||||
|
||||
protected static UnitEnum|string|null $navigationGroup = null;
|
||||
|
||||
public string $step = 'intro';
|
||||
|
||||
public array $status = [];
|
||||
|
||||
public array $inviteDownloads = [];
|
||||
|
||||
public array $selectedPackages = [];
|
||||
|
||||
public string $eventName = '';
|
||||
|
||||
public ?string $eventDate = null;
|
||||
|
||||
public ?int $eventTypeId = null;
|
||||
|
||||
public ?string $palette = null;
|
||||
|
||||
public ?string $inviteLayout = null;
|
||||
|
||||
public bool $isProcessing = false;
|
||||
|
||||
protected static bool $shouldRegisterNavigation = true;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$tenant = TenantOnboardingState::tenant();
|
||||
|
||||
abort_if(! $tenant, 403);
|
||||
|
||||
$this->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<TaskCollection> $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('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');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user