completed the frontend dashboard component and bound it to the tenant admin pwa for the optimal onboarding experience.. Added a profile page.
This commit is contained in:
125
app/Http/Controllers/Api/Tenant/OnboardingController.php
Normal file
125
app/Http/Controllers/Api/Tenant/OnboardingController.php
Normal file
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Tenant;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Tenant;
|
||||
use App\Support\TenantAuth;
|
||||
use App\Support\TenantOnboardingState;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class OnboardingController extends Controller
|
||||
{
|
||||
public function show(Request $request): JsonResponse
|
||||
{
|
||||
$tenant = $this->resolveTenant($request);
|
||||
|
||||
if (! $tenant) {
|
||||
return response()->json([
|
||||
'error' => 'tenant_context_missing',
|
||||
'message' => 'Der Tenant-Kontext konnte nicht ermittelt werden.',
|
||||
], Response::HTTP_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
$status = TenantOnboardingState::status($tenant);
|
||||
$settings = $tenant->settings ?? [];
|
||||
|
||||
return response()->json([
|
||||
'steps' => [
|
||||
'admin_app_opened_at' => Arr::get($settings, 'onboarding.admin_app_opened_at'),
|
||||
'primary_event_id' => Arr::get($settings, 'onboarding.primary_event_id'),
|
||||
'selected_packages' => Arr::get($settings, 'onboarding.selected_packages'),
|
||||
'branding_completed' => (bool) ($status['palette'] ?? false),
|
||||
'tasks_configured' => (bool) ($status['packages'] ?? false),
|
||||
'event_created' => (bool) ($status['event'] ?? false),
|
||||
'invite_created' => (bool) ($status['invite'] ?? false),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$tenant = $this->resolveTenant($request);
|
||||
|
||||
if (! $tenant) {
|
||||
return response()->json([
|
||||
'error' => 'tenant_context_missing',
|
||||
'message' => 'Der Tenant-Kontext konnte nicht ermittelt werden.',
|
||||
], Response::HTTP_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
$payload = $request->validate([
|
||||
'step' => ['required', 'string'],
|
||||
'meta' => ['array'],
|
||||
]);
|
||||
|
||||
$step = $payload['step'];
|
||||
$meta = $payload['meta'] ?? [];
|
||||
|
||||
$settings = $tenant->settings ?? [];
|
||||
|
||||
switch ($step) {
|
||||
case 'admin_app_opened':
|
||||
Arr::set($settings, 'onboarding.admin_app_opened_at', Carbon::now()->toIso8601String());
|
||||
break;
|
||||
|
||||
case 'event_created':
|
||||
Arr::set($settings, 'onboarding.primary_event_id', Arr::get($meta, 'event_id'));
|
||||
break;
|
||||
|
||||
case 'package_selected':
|
||||
Arr::set($settings, 'onboarding.selected_packages', Arr::get($meta, 'packages'));
|
||||
break;
|
||||
|
||||
case 'branding_configured':
|
||||
Arr::set($settings, 'onboarding.branding_set', true);
|
||||
break;
|
||||
|
||||
case 'invite_created':
|
||||
Arr::set($settings, 'onboarding.invite_created_at', Carbon::now()->toIso8601String());
|
||||
break;
|
||||
|
||||
case 'completed':
|
||||
TenantOnboardingState::markCompleted($tenant, $meta);
|
||||
break;
|
||||
|
||||
default:
|
||||
Log::info('[TenantOnboarding] Unbekannter Schritt gemeldet', [
|
||||
'tenant_id' => $tenant->id,
|
||||
'step' => $step,
|
||||
]);
|
||||
}
|
||||
|
||||
if ($step !== 'completed') {
|
||||
$tenant->forceFill(['settings' => $settings])->save();
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Onboarding-Schritt aktualisiert.',
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveTenant(Request $request): ?Tenant
|
||||
{
|
||||
try {
|
||||
$tenantId = $request->attributes->get('tenant_id');
|
||||
|
||||
if ($tenantId) {
|
||||
return Tenant::query()->find($tenantId);
|
||||
}
|
||||
|
||||
return TenantAuth::resolveTenant($request);
|
||||
} catch (\Throwable $exception) {
|
||||
Log::warning('[TenantOnboarding] Tenant konnte nicht ermittelt werden', [
|
||||
'error' => $exception->getMessage(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,12 @@ namespace App\Http\Controllers;
|
||||
use App\Models\Event;
|
||||
use App\Models\PackagePurchase;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\TenantPackage;
|
||||
use App\Services\Tenant\DashboardSummaryService;
|
||||
use App\Support\TenantOnboardingState;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
@@ -32,6 +35,9 @@ class DashboardController extends Controller
|
||||
: collect();
|
||||
|
||||
$activePackage = $summary['active_package'] ?? null;
|
||||
$onboarding = $tenant instanceof Tenant
|
||||
? $this->buildOnboardingSteps($tenant, $summary ?? [])
|
||||
: [];
|
||||
|
||||
return Inertia::render('dashboard', [
|
||||
'metrics' => $summary,
|
||||
@@ -41,7 +47,6 @@ class DashboardController extends Controller
|
||||
'tenant' => $tenant ? [
|
||||
'id' => $tenant->id,
|
||||
'name' => $tenant->name,
|
||||
'eventCreditsBalance' => $tenant->event_credits_balance,
|
||||
'subscriptionStatus' => $tenant->subscription_status,
|
||||
'subscriptionExpiresAt' => optional($tenant->subscription_expires_at)->toIso8601String(),
|
||||
'activePackage' => $activePackage ? [
|
||||
@@ -49,12 +54,14 @@ class DashboardController extends Controller
|
||||
'price' => $activePackage['price'] ?? null,
|
||||
'expiresAt' => $activePackage['expires_at'] ?? null,
|
||||
'remainingEvents' => $activePackage['remaining_events'] ?? null,
|
||||
'isActive' => (bool) ($activePackage['is_active'] ?? false),
|
||||
] : null,
|
||||
] : null,
|
||||
'emailVerification' => [
|
||||
'mustVerify' => $user instanceof MustVerifyEmail,
|
||||
'verified' => $user?->hasVerifiedEmail() ?? false,
|
||||
],
|
||||
'onboarding' => $onboarding,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -84,23 +91,120 @@ class DashboardController extends Controller
|
||||
|
||||
private function collectRecentPurchases(Tenant $tenant): Collection
|
||||
{
|
||||
return $tenant->purchases()
|
||||
->with('package')
|
||||
->latest('purchased_at')
|
||||
->limit(6)
|
||||
->get()
|
||||
->map(function (PackagePurchase $purchase): array {
|
||||
return [
|
||||
'id' => $purchase->id,
|
||||
'packageName' => $purchase->package?->getNameForLocale(app()->getLocale())
|
||||
?? $purchase->package?->name
|
||||
?? __('Unknown package'),
|
||||
'price' => $purchase->price !== null ? (float) $purchase->price : null,
|
||||
'purchasedAt' => optional($purchase->purchased_at)->toIso8601String(),
|
||||
'type' => $purchase->type,
|
||||
'provider' => $purchase->provider,
|
||||
];
|
||||
});
|
||||
$entries = collect(
|
||||
$tenant->purchases()
|
||||
->with('package')
|
||||
->latest('purchased_at')
|
||||
->limit(6)
|
||||
->get()
|
||||
->map(function (PackagePurchase $purchase): array {
|
||||
return [
|
||||
'id' => $purchase->id,
|
||||
'packageName' => $purchase->package?->getNameForLocale(app()->getLocale())
|
||||
?? $purchase->package?->name
|
||||
?? __('Unknown package'),
|
||||
'price' => $purchase->price !== null ? (float) $purchase->price : null,
|
||||
'purchasedAt' => optional($purchase->purchased_at)->toIso8601String(),
|
||||
'type' => $purchase->type,
|
||||
'provider' => $purchase->provider,
|
||||
'source' => 'purchase',
|
||||
'isActive' => null,
|
||||
];
|
||||
})
|
||||
->all()
|
||||
);
|
||||
|
||||
if ($entries->isEmpty()) {
|
||||
$packageEntries = collect(
|
||||
$tenant->tenantPackages()
|
||||
->with('package')
|
||||
->orderByDesc('purchased_at')
|
||||
->limit(6)
|
||||
->get()
|
||||
->map(function (TenantPackage $package): array {
|
||||
return [
|
||||
'id' => $package->id,
|
||||
'packageName' => $package->package?->getNameForLocale(app()->getLocale())
|
||||
?? $package->package?->name
|
||||
?? __('Unknown package'),
|
||||
'price' => $package->price !== null ? (float) $package->price : null,
|
||||
'purchasedAt' => optional($package->purchased_at)->toIso8601String(),
|
||||
'type' => 'tenant_package',
|
||||
'provider' => 'internal',
|
||||
'source' => 'tenant_package',
|
||||
'isActive' => (bool) $package->active,
|
||||
];
|
||||
})
|
||||
->all()
|
||||
);
|
||||
|
||||
$entries = $entries->merge($packageEntries);
|
||||
}
|
||||
|
||||
return $entries
|
||||
->sortByDesc('purchasedAt')
|
||||
->values()
|
||||
->take(6);
|
||||
}
|
||||
|
||||
private function buildOnboardingSteps(Tenant $tenant, array $summary): array
|
||||
{
|
||||
$status = TenantOnboardingState::status($tenant);
|
||||
$settings = $tenant->settings ?? [];
|
||||
|
||||
$adminAppOpened = (bool) Arr::get($settings, 'onboarding.admin_app_opened_at')
|
||||
|| $tenant->last_activity_at !== null;
|
||||
|
||||
$hasEvent = (bool) ($status['event'] ?? false);
|
||||
$hasInvite = (bool) ($status['invite'] ?? false);
|
||||
$hasBranding = (bool) ($status['palette'] ?? false)
|
||||
|| (bool) Arr::get($settings, 'onboarding.branding_set', false);
|
||||
$hasTaskPackage = (bool) ($status['packages'] ?? false)
|
||||
|| ! empty(Arr::get($settings, 'onboarding.selected_packages'));
|
||||
$hasPhotos = $tenant->photos()->exists() || (int) ($summary['new_photos'] ?? 0) > 0;
|
||||
|
||||
return [
|
||||
[
|
||||
'key' => 'admin_app',
|
||||
'title' => trans('dashboard.onboarding.admin_app.title'),
|
||||
'description' => trans('dashboard.onboarding.admin_app.description'),
|
||||
'done' => $adminAppOpened,
|
||||
'cta' => url('/event-admin'),
|
||||
'ctaLabel' => trans('dashboard.onboarding.admin_app.cta'),
|
||||
],
|
||||
[
|
||||
'key' => 'event_setup',
|
||||
'title' => trans('dashboard.onboarding.event_setup.title'),
|
||||
'description' => trans('dashboard.onboarding.event_setup.description'),
|
||||
'done' => $hasEvent,
|
||||
'cta' => url('/event-admin/events/new'),
|
||||
'ctaLabel' => trans('dashboard.onboarding.event_setup.cta'),
|
||||
],
|
||||
[
|
||||
'key' => 'invite_guests',
|
||||
'title' => trans('dashboard.onboarding.invite_guests.title'),
|
||||
'description' => trans('dashboard.onboarding.invite_guests.description'),
|
||||
'done' => $hasInvite,
|
||||
'cta' => url('/event-admin/events'),
|
||||
'ctaLabel' => trans('dashboard.onboarding.invite_guests.cta'),
|
||||
],
|
||||
[
|
||||
'key' => 'collect_photos',
|
||||
'title' => trans('dashboard.onboarding.collect_photos.title'),
|
||||
'description' => trans('dashboard.onboarding.collect_photos.description'),
|
||||
'done' => $hasPhotos,
|
||||
'cta' => url('/event-admin/dashboard'),
|
||||
'ctaLabel' => trans('dashboard.onboarding.collect_photos.cta'),
|
||||
],
|
||||
[
|
||||
'key' => 'branding',
|
||||
'title' => trans('dashboard.onboarding.branding.title'),
|
||||
'description' => trans('dashboard.onboarding.branding.description'),
|
||||
'done' => $hasBranding && $hasTaskPackage,
|
||||
'cta' => url('/event-admin/tasks'),
|
||||
'ctaLabel' => trans('dashboard.onboarding.branding.cta'),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function resolveEventName(Event $event): string
|
||||
|
||||
@@ -56,7 +56,6 @@ class ProfileController extends Controller
|
||||
'tenant' => $tenant ? [
|
||||
'id' => $tenant->id,
|
||||
'name' => $tenant->name,
|
||||
'eventCreditsBalance' => $tenant->event_credits_balance,
|
||||
'subscriptionStatus' => $tenant->subscription_status,
|
||||
'subscriptionExpiresAt' => optional($tenant->subscription_expires_at)->toIso8601String(),
|
||||
'activePackage' => $activePackage ? [
|
||||
|
||||
Reference in New Issue
Block a user