libra/docs/stories/story-6.1-dashboard-overvie...

9.5 KiB

Story 6.1: Dashboard Overview & Statistics

Epic Reference

Epic 6: Admin Dashboard

User Story

As an admin, I want to see real-time metrics and key statistics at a glance, So that I can understand the current state of my practice.

Prerequisites / Dependencies

This story requires the following to be completed first:

Dependency Required From What's Needed
User Model Epic 2 status (active/deactivated), user_type (individual/company) fields
Consultation Model Epic 3 consultations table with status, consultation_type, scheduled_date
Timeline Model Epic 4 timelines table with status (active/archived)
Timeline Updates Epic 4 timeline_updates table with created_at
Post Model Epic 5 posts table with status (published/draft), created_at
Admin Layout Epic 1 Admin authenticated layout with navigation

References:

  • Epic 6 details: docs/epics/epic-6-admin-dashboard.md
  • PRD Dashboard Section: docs/prd.md Section 5.7 (Admin Dashboard)
  • Database Schema: docs/prd.md Section 16.1

Acceptance Criteria

User Metrics Card

  • Total active clients (individual + company with status = active)
  • Individual vs company breakdown (count each type)
  • Deactivated clients count
  • New clients this month (created_at in current month)

Booking Metrics Card

  • Pending requests count (highlighted with warning color)
  • Today's consultations (scheduled for today, approved status)
  • This week's consultations
  • This month's consultations
  • Free vs paid breakdown (consultation_type field)
  • No-show rate percentage (no-show / total completed * 100)

Timeline Metrics Card

  • Active case timelines (status = active)
  • Archived timelines (status = archived)
  • Updates added this week (timeline_updates created in last 7 days)

Posts Metrics Card

  • Total published posts (status = published)
  • Posts published this month

Design

  • Clean card-based layout using Flux UI components
  • Color-coded status indicators (gold for highlights, success green, warning colors)
  • Responsive grid (2 columns on tablet, 1 on mobile, 4 on desktop)
  • Navy blue and gold color scheme per PRD Section 7.1

Technical Implementation

Files to Create/Modify

File Purpose
resources/views/livewire/admin/dashboard.blade.php Main Volt component
routes/web.php Add admin dashboard route

Route Definition

Route::middleware(['auth', 'admin'])->prefix('admin')->group(function () {
    Route::get('/dashboard', function () {
        return view('livewire.admin.dashboard');
    })->name('admin.dashboard');
});

Component Structure (Volt Class-Based)

<?php

use App\Models\User;
use App\Models\Consultation;
use App\Models\Timeline;
use App\Models\TimelineUpdate;
use App\Models\Post;
use Illuminate\Support\Facades\Cache;
use Livewire\Volt\Component;

new class extends Component {
    public function with(): array
    {
        return [
            'userMetrics' => $this->getUserMetrics(),
            'bookingMetrics' => $this->getBookingMetrics(),
            'timelineMetrics' => $this->getTimelineMetrics(),
            'postMetrics' => $this->getPostMetrics(),
        ];
    }

    private function getUserMetrics(): array
    {
        return Cache::remember('admin.metrics.users', 300, fn() => [
            'total_active' => User::where('status', 'active')
                ->whereIn('user_type', ['individual', 'company'])->count(),
            'individual' => User::where('user_type', 'individual')
                ->where('status', 'active')->count(),
            'company' => User::where('user_type', 'company')
                ->where('status', 'active')->count(),
            'deactivated' => User::where('status', 'deactivated')->count(),
            'new_this_month' => User::whereMonth('created_at', now()->month)
                ->whereYear('created_at', now()->year)->count(),
        ]);
    }

    private function getBookingMetrics(): array
    {
        return Cache::remember('admin.metrics.bookings', 300, function () {
            $total = Consultation::whereIn('status', ['completed', 'no-show'])->count();
            $noShows = Consultation::where('status', 'no-show')->count();

            return [
                'pending' => Consultation::where('status', 'pending')->count(),
                'today' => Consultation::whereDate('scheduled_date', today())
                    ->where('status', 'approved')->count(),
                'this_week' => Consultation::whereBetween('scheduled_date', [
                    now()->startOfWeek(), now()->endOfWeek()
                ])->count(),
                'this_month' => Consultation::whereMonth('scheduled_date', now()->month)
                    ->whereYear('scheduled_date', now()->year)->count(),
                'free' => Consultation::where('consultation_type', 'free')->count(),
                'paid' => Consultation::where('consultation_type', 'paid')->count(),
                'no_show_rate' => $total > 0 ? round(($noShows / $total) * 100, 1) : 0,
            ];
        });
    }

    private function getTimelineMetrics(): array
    {
        return Cache::remember('admin.metrics.timelines', 300, fn() => [
            'active' => Timeline::where('status', 'active')->count(),
            'archived' => Timeline::where('status', 'archived')->count(),
            'updates_this_week' => TimelineUpdate::where('created_at', '>=', now()->subWeek())->count(),
        ]);
    }

    private function getPostMetrics(): array
    {
        return Cache::remember('admin.metrics.posts', 300, fn() => [
            'total_published' => Post::where('status', 'published')->count(),
            'this_month' => Post::where('status', 'published')
                ->whereMonth('created_at', now()->month)
                ->whereYear('created_at', now()->year)->count(),
        ]);
    }
}; ?>

<div>
    {{-- Dashboard content with Flux UI cards --}}
</div>

Flux UI Components to Use

  • <flux:heading> - Page title
  • <flux:card> or custom card component - Metric cards (if available, otherwise Tailwind)
  • <flux:badge> - Status indicators
  • <flux:text> - Metric labels and values

Cache Strategy

  • TTL: 300 seconds (5 minutes) for all metrics
  • Keys: admin.metrics.users, admin.metrics.bookings, admin.metrics.timelines, admin.metrics.posts
  • Invalidation: Consider cache clearing when data changes (optional enhancement)

Edge Cases & Error Handling

Scenario Expected Behavior
Empty database (0 clients) All metrics display "0" - no errors
No consultations exist No-show rate displays "0%" (not "N/A" or error)
New month (1st day) "This month" metrics show 0
Cache failure Queries execute directly without caching (graceful degradation)
Division by zero (no-show rate) Return 0 when total consultations is 0

Testing Requirements

Test File

tests/Feature/Admin/DashboardTest.php

Test Scenarios

// 1. Dashboard loads successfully for admin
test('admin can view dashboard', function () {
    $admin = User::factory()->admin()->create();

    $this->actingAs($admin)
        ->get(route('admin.dashboard'))
        ->assertSuccessful()
        ->assertSee('Dashboard');
});

// 2. Metrics display correctly with sample data
test('dashboard displays correct user metrics', function () {
    User::factory()->count(5)->create(['status' => 'active', 'user_type' => 'individual']);
    User::factory()->count(3)->create(['status' => 'active', 'user_type' => 'company']);
    User::factory()->count(2)->create(['status' => 'deactivated']);

    // Assert metrics show 8 active, 5 individual, 3 company, 2 deactivated
});

// 3. Empty state handling
test('dashboard handles empty database gracefully', function () {
    $admin = User::factory()->admin()->create();

    $this->actingAs($admin)
        ->get(route('admin.dashboard'))
        ->assertSuccessful()
        ->assertSee('0'); // Should show zeros, not errors
});

// 4. No-show rate calculation
test('no-show rate calculates correctly', function () {
    // Create 10 completed, 2 no-shows = 20% rate
});

// 5. Cache behavior
test('metrics are cached for 5 minutes', function () {
    // Verify cache key exists after first load
});

// 6. Non-admin cannot access
test('non-admin cannot access dashboard', function () {
    $client = User::factory()->client()->create();

    $this->actingAs($client)
        ->get(route('admin.dashboard'))
        ->assertForbidden();
});

Manual Testing Checklist

  • Verify responsive layout on mobile (375px)
  • Verify responsive layout on tablet (768px)
  • Verify responsive layout on desktop (1200px+)
  • Verify pending count is highlighted/prominent
  • Verify color scheme matches PRD (navy blue, gold)
  • Verify RTL layout works correctly (Arabic)

Definition of Done

  • All metric cards display correctly with accurate data
  • Data is cached with 5-minute TTL
  • Empty states handled gracefully (show 0, no errors)
  • No-show rate handles division by zero
  • Responsive layout works on mobile, tablet, desktop
  • Color scheme matches brand guidelines
  • All tests pass
  • Admin-only access enforced
  • Code formatted with Pint

Estimation

Complexity: Medium | Effort: 4-5 hours

Out of Scope

  • Real-time updates (polling/websockets) - covered in Story 6.3
  • Charts and visualizations - covered in Story 6.2
  • Quick action buttons - covered in Story 6.3