Es gibt nun task collections und vordefinierte tasks für alle. Onboarding verfeinert und webseite-carousel gefixt (logging später entfernen!)
312 lines
9.6 KiB
PHP
312 lines
9.6 KiB
PHP
<?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');
|
||
}
|
||
}
|