admin = User::factory()->admin()->create(); Cache::flush(); }); // =========================================== // Access Control Tests // =========================================== test('admin can view analytics charts section', function () { $this->actingAs($this->admin) ->get(route('admin.dashboard')) ->assertSuccessful() ->assertSee(__('admin_metrics.analytics_charts')) ->assertSee(__('admin_metrics.monthly_trends')); }); test('non-admin cannot access analytics charts', function () { $client = User::factory()->individual()->create(); $this->actingAs($client) ->get(route('admin.dashboard')) ->assertForbidden(); }); // =========================================== // AnalyticsService Unit Tests // =========================================== test('analytics service returns correct monthly client counts', function () { // Create clients across different months User::factory()->individual()->create([ 'created_at' => now()->subMonths(2)->startOfMonth()->addDays(5), ]); User::factory()->count(3)->individual()->create([ 'created_at' => now()->subMonth()->startOfMonth()->addDays(5), ]); User::factory()->count(2)->company()->create([ 'created_at' => now()->startOfMonth()->addDays(5), ]); $service = new AnalyticsService; $data = $service->getMonthlyNewClients(now()->subMonths(2)->startOfMonth(), 3); expect($data)->toBe([1, 3, 2]); }); test('analytics service returns correct monthly consultation counts', function () { Consultation::factory()->count(2)->create([ 'booking_date' => now()->subMonths(2)->startOfMonth()->addDays(5), ]); Consultation::factory()->count(4)->create([ 'booking_date' => now()->subMonth()->startOfMonth()->addDays(5), ]); Consultation::factory()->count(3)->create([ 'booking_date' => now()->startOfMonth()->addDays(5), ]); $service = new AnalyticsService; $data = $service->getMonthlyConsultations(now()->subMonths(2)->startOfMonth(), 3); expect($data)->toBe([2, 4, 3]); }); test('consultation breakdown calculates free vs paid correctly', function () { Consultation::factory()->count(5)->free()->create([ 'booking_date' => now()->subMonth(), ]); Consultation::factory()->count(3)->paid()->create([ 'booking_date' => now()->subMonth(), ]); $service = new AnalyticsService; $breakdown = $service->getConsultationTypeBreakdown(now()->subYear()->startOfMonth(), 12); expect($breakdown['free'])->toBe(5) ->and($breakdown['paid'])->toBe(3); }); test('no-show rate calculates correctly', function () { // Create 8 completed, 2 no-shows = 20% rate Consultation::factory()->count(8)->completed()->create([ 'booking_date' => now()->startOfMonth()->addDays(5), ]); Consultation::factory()->count(2)->noShow()->create([ 'booking_date' => now()->startOfMonth()->addDays(5), ]); $service = new AnalyticsService; $rates = $service->getMonthlyNoShowRates(now()->startOfMonth(), 1); expect($rates[0])->toBe(20.0); }); test('no-show rate returns zero when no consultations exist', function () { $service = new AnalyticsService; $rates = $service->getMonthlyNoShowRates(now()->startOfMonth(), 1); expect($rates[0])->toBe(0); }); test('no-show rate handles zero completed consultations', function () { // Only create pending consultations (not completed or no-show) Consultation::factory()->count(5)->pending()->create([ 'booking_date' => now()->startOfMonth()->addDays(5), ]); $service = new AnalyticsService; $rates = $service->getMonthlyNoShowRates(now()->startOfMonth(), 1); expect($rates[0])->toBe(0); }); test('month labels are generated correctly', function () { $service = new AnalyticsService; $labels = $service->getMonthLabels(now()->startOfMonth(), 3); expect($labels)->toHaveCount(3) ->and($labels[0])->toBe(now()->startOfMonth()->translatedFormat('M Y')) ->and($labels[1])->toBe(now()->addMonth()->translatedFormat('M Y')) ->and($labels[2])->toBe(now()->addMonths(2)->translatedFormat('M Y')); }); // =========================================== // Dashboard Component Chart Data Tests // =========================================== test('dashboard provides chart data', function () { $this->actingAs($this->admin); $component = Volt::test('admin.dashboard'); $chartData = $component->viewData('chartData'); expect($chartData)->toHaveKeys(['labels', 'newClients', 'consultations', 'consultationBreakdown', 'noShowRates']) ->and($chartData['consultationBreakdown'])->toHaveKeys(['free', 'paid']); }); test('chart data defaults to 6 months', function () { $this->actingAs($this->admin); $component = Volt::test('admin.dashboard'); $chartData = $component->viewData('chartData'); expect($chartData['labels'])->toHaveCount(6) ->and($chartData['newClients'])->toHaveCount(6) ->and($chartData['consultations'])->toHaveCount(6) ->and($chartData['noShowRates'])->toHaveCount(6); }); test('date range selector changes chart period to 12 months', function () { $this->actingAs($this->admin); $component = Volt::test('admin.dashboard') ->assertSet('chartPeriod', '6m') ->set('chartPeriod', '12m') ->assertSet('chartPeriod', '12m'); $chartData = $component->viewData('chartData'); expect($chartData['labels'])->toHaveCount(12) ->and($chartData['newClients'])->toHaveCount(12) ->and($chartData['consultations'])->toHaveCount(12) ->and($chartData['noShowRates'])->toHaveCount(12); }); test('custom date range can be set', function () { $this->actingAs($this->admin); $start = now()->subMonths(3)->format('Y-m'); $end = now()->format('Y-m'); $component = Volt::test('admin.dashboard') ->set('customStartMonth', $start) ->set('customEndMonth', $end) ->call('setCustomRange') ->assertSet('chartPeriod', 'custom'); $chartData = $component->viewData('chartData'); expect($chartData['labels'])->toHaveCount(4); // 4 months inclusive }); test('chart handles empty data gracefully', function () { $this->actingAs($this->admin); // No clients or consultations created $component = Volt::test('admin.dashboard'); $chartData = $component->viewData('chartData'); // Should have all zeros but not error expect(array_sum($chartData['newClients']))->toBe(0) ->and(array_sum($chartData['consultations']))->toBe(0) ->and($chartData['consultationBreakdown']['free'])->toBe(0) ->and($chartData['consultationBreakdown']['paid'])->toBe(0) ->and(array_sum($chartData['noShowRates']))->toBe(0); }); // =========================================== // UI Element Tests // =========================================== test('dashboard displays chart section with date range buttons', function () { $this->actingAs($this->admin) ->get(route('admin.dashboard')) ->assertSee(__('admin_metrics.last_6_months')) ->assertSee(__('admin_metrics.last_12_months')) ->assertSee(__('admin_metrics.apply')); }); test('dashboard displays all chart cards', function () { $this->actingAs($this->admin) ->get(route('admin.dashboard')) ->assertSee(__('admin_metrics.monthly_trends')) ->assertSee(__('admin_metrics.consultation_breakdown')) ->assertSee(__('admin_metrics.noshow_rate_trend')); }); test('empty state message shown when no data', function () { $this->actingAs($this->admin) ->get(route('admin.dashboard')) ->assertSee(__('admin_metrics.no_data_available')); }); // =========================================== // Data Accuracy Tests // =========================================== test('chart data excludes admin users from client counts', function () { // Create admin users (should not be counted) User::factory()->count(2)->admin()->create([ 'created_at' => now()->startOfMonth()->addDays(5), ]); // Create individual clients (should be counted) User::factory()->count(3)->individual()->create([ 'created_at' => now()->startOfMonth()->addDays(5), ]); $service = new AnalyticsService; $data = $service->getMonthlyNewClients(now()->startOfMonth(), 1); expect($data[0])->toBe(3); }); test('chart data correctly filters by date range', function () { // Create consultations outside the range Consultation::factory()->count(5)->create([ 'booking_date' => now()->subYears(2), ]); // Create consultations inside the range Consultation::factory()->count(3)->create([ 'booking_date' => now()->subMonth()->startOfMonth()->addDays(5), ]); $service = new AnalyticsService; $data = $service->getMonthlyConsultations(now()->subMonths(2)->startOfMonth(), 3); // Only the 3 consultations in range should be counted expect(array_sum($data))->toBe(3); });