diff --git a/resources/views/components/portal/onboarding-guide.blade.php b/resources/views/components/portal/onboarding-guide.blade.php new file mode 100644 index 00000000..369efe8f --- /dev/null +++ b/resources/views/components/portal/onboarding-guide.blade.php @@ -0,0 +1,112 @@ +@props([ + /** + * Liste der Onboarding-Schritte. + * + * @var list + */ + 'steps' => [], + + /** Anzahl bereits erledigter Schritte */ + 'completed' => 0, + + /** Gesamtzahl der Schritte */ + 'total' => 0, +]) + +@php + $percent = $total > 0 ? (int) round(($completed / $total) * 100) : 0; + $allDone = $total > 0 && $completed >= $total; +@endphp + +
+ + +
+ +
+
diff --git a/resources/views/errors/layout.blade.php b/resources/views/errors/layout.blade.php index c21d8720..e3c1364c 100644 --- a/resources/views/errors/layout.blade.php +++ b/resources/views/errors/layout.blade.php @@ -11,23 +11,34 @@ @yield('code') · @yield('title') – {{ $domainName ?? 'Pressekonto' }} diff --git a/resources/views/livewire/customer/dashboard.blade.php b/resources/views/livewire/customer/dashboard.blade.php index e243c2d2..0c7dc76f 100644 --- a/resources/views/livewire/customer/dashboard.blade.php +++ b/resources/views/livewire/customer/dashboard.blade.php @@ -50,9 +50,20 @@ new #[Layout('components.layouts.app'), Title('Mein Dashboard')] class extends C $profile = $user->profile; $billingAddress = $user->billingAddress; + $profilePercent = $this->profileCompleteness($profile); + $billingPercent = $this->billingCompleteness($billingAddress); + $pressReleaseTotal = (int) ($stats->total ?? 0); + $companiesTotal = $context->companyCountFor($user); + + $onboardingSteps = $this->onboardingSteps($profilePercent, $billingPercent, $selectedCompany, $companiesTotal, $pressReleaseTotal); + $onboardingCompleted = collect($onboardingSteps)->where('done', true)->count(); + return [ 'user' => $user, 'selectedCompany' => $selectedCompany, + 'onboardingSteps' => $onboardingSteps, + 'onboardingCompleted' => $onboardingCompleted, + 'onboardingTotal' => count($onboardingSteps), // Launch-Gate (WS-5): Solange billing.enforce_booking aus ist, ist // dies immer false und der Hinweis bleibt unsichtbar. Bei scharfem // Gate sehen Bestandskunden (grandfathered) den Banner nicht. @@ -64,12 +75,12 @@ new #[Layout('components.layouts.app'), Title('Mein Dashboard')] class extends C 'draft' => (int) ($stats->draft ?? 0), 'deltaMonth' => (int) ($stats->current_month ?? 0) - (int) ($stats->previous_month ?? 0), ], - 'profileCompleteness' => $this->profileCompleteness($profile), - 'billingCompleteness' => $this->billingCompleteness($billingAddress), + 'profileCompleteness' => $profilePercent, + 'billingCompleteness' => $billingPercent, 'qualityHints' => $this->qualityHints($user, $profile, $billingAddress, $selectedCompany, $pressReleaseQuery), 'recent' => $recent, 'companies' => $context->latestCompaniesFor($user), - 'companiesTotal' => $context->companyCountFor($user), + 'companiesTotal' => $companiesTotal, 'bridgeStatus' => [ /* Heute hardcoded — perspektivisch aus echtem Sync-Service. */ 'presseecho' => ['state' => 'connected', 'subline' => __('Archiv · Branchen-Tiefe')], @@ -104,6 +115,54 @@ new #[Layout('components.layouts.app'), Title('Mein Dashboard')] class extends C return $address?->completenessPercent() ?? 0; } + /** + * Geführte „Erste Schritte" für neue Kunden. Jeder Schritt gilt als + * erledigt, sobald die zugehörigen Stammdaten vorhanden sind. + * + * @return list + */ + private function onboardingSteps(int $profilePercent, int $billingPercent, ?Company $selectedCompany, int $companiesTotal, int $pressReleaseTotal): array + { + return [ + [ + 'num' => '01', + 'icon' => 'user', + 'title' => __('Profil vervollständigen'), + 'description' => __('Hinterlegen Sie Anrede, Telefon und Adresse für eine saubere Kundenakte.'), + 'href' => route('me.profile') . '#profil', + 'action' => __('Profil öffnen'), + 'done' => $profilePercent >= 100, + ], + [ + 'num' => '02', + 'icon' => 'archive-box', + 'title' => __('Rechnungsadresse hinterlegen'), + 'description' => __('Damit spätere Buchungen sauber abgerechnet werden können.'), + 'href' => route('me.profile') . '#rechnungsadresse', + 'action' => __('Adresse ergänzen'), + 'done' => $billingPercent >= 100, + ], + [ + 'num' => '03', + 'icon' => 'building-office', + 'title' => __('Firma zuordnen'), + 'description' => __('Ordnen Sie Ihrem Konto eine Firma bzw. Pressestelle zu, für die Sie veröffentlichen.'), + 'href' => route('me.press-kits.index'), + 'action' => __('Firmen öffnen'), + 'done' => $selectedCompany !== null || $companiesTotal > 0, + ], + [ + 'num' => '04', + 'icon' => 'newspaper', + 'title' => __('Erste Pressemitteilung erstellen'), + 'description' => __('Verfassen Sie Ihre erste Mitteilung und senden Sie sie zur Prüfung.'), + 'href' => route('me.press-releases.create'), + 'action' => __('Mitteilung erstellen'), + 'done' => $pressReleaseTotal > 0, + ], + ]; + } + /** * @return list */ @@ -205,6 +264,11 @@ new #[Layout('components.layouts.app'), Title('Mein Dashboard')] class extends C + {{-- ============== ERSTE SCHRITTE (Onboarding, aufklappbar) ============== --}} + @if ($onboardingCompleted < $onboardingTotal) + + @endif + {{-- ============== BUCHUNGS-GATE (WS-5) ============== --}} @if ($bookingRequired)
assertSee(__('Zur Prüfung senden')); }); +test('the first-steps onboarding card appears for a fresh customer with all steps open', function () { + /** @var TestCase $this */ + $customer = User::factory()->create(['is_active' => true]); + $customer->assignRole('customer'); + + $this->actingAs($customer); + + LivewireVolt::test('customer.dashboard') + ->assertSee(__('Erste Schritte')) + ->assertSee('0/4 '.__('erledigt')) + ->assertSee(__('Profil vervollständigen')) + ->assertSee(__('Rechnungsadresse hinterlegen')) + ->assertSee(__('Firma zuordnen')) + ->assertSee(__('Erste Pressemitteilung erstellen')); +}); + +test('the first-steps onboarding card reflects completed steps in the counter', function () { + /** @var TestCase $this */ + $customer = User::factory()->create(['is_active' => true]); + $customer->assignRole('customer'); + + Company::factory()->create(['owner_user_id' => $customer->id]); + + $this->actingAs($customer); + + LivewireVolt::test('customer.dashboard') + ->assertSee(__('Erste Schritte')) + ->assertSee('1/4 '.__('erledigt')) + ->assertSee(__('Erledigt')); +}); + +test('the first-steps onboarding card disappears once every step is complete', function () { + /** @var TestCase $this */ + $customer = User::factory()->create(['is_active' => true]); + $customer->assignRole('customer'); + + Profile::factory()->create([ + 'user_id' => $customer->id, + 'salutation_key' => 'mr', + 'first_name' => 'Test', + 'last_name' => 'Customer', + 'phone' => '+49 30 1234567', + 'address' => 'Teststr. 1', + 'country_code' => 'DE', + ]); + + BillingAddress::query()->create([ + 'user_id' => $customer->id, + 'name' => 'Test Customer GmbH', + 'address1' => 'Teststr. 1', + 'postal_code' => '10115', + 'city' => 'Berlin', + 'country_code' => 'DE', + ]); + + $company = Company::factory()->create(['owner_user_id' => $customer->id]); + + PressRelease::factory()->create([ + 'user_id' => $customer->id, + 'company_id' => $company->id, + ]); + + $this->actingAs($customer); + + LivewireVolt::test('customer.dashboard') + ->assertDontSee(__('Erste Schritte')); +}); + test('press release list and KPI counts render when data exists', function () { /** @var TestCase $this */ $customer = User::factory()->create(['is_active' => true]);