libra/docs/stories/story-7.1-client-dashboard-...

10 KiB

Story 7.1: Client Dashboard Overview

Epic Reference

Epic 7: Client Dashboard

User Story

As a client, I want a dashboard showing my key information at a glance, So that I can quickly see upcoming consultations and case updates.

Dependencies

Epic Dependencies

Epic Dependency Status
Epic 1 Authentication system, base UI layout Required
Epic 2 User model with client data Required
Epic 3 Consultation model and booking system Required
Epic 4 Timeline and TimelineUpdate models Required

Model Prerequisites

The following models and relationships must exist before implementation:

User Model (app/Models/User.php):

  • consultations() - HasMany relationship to Consultation
  • timelines() - HasMany relationship to Timeline

Consultation Model (app/Models/Consultation.php):

  • approved() scope - filters status = 'approved'
  • pending() scope - filters status = 'pending'
  • upcoming() scope - filters scheduled_date >= today()
  • scheduled_date column (date)
  • scheduled_time column (time)
  • type column (enum: 'free', 'paid')
  • status column (enum: 'pending', 'approved', 'rejected', 'completed', 'no-show', 'cancelled')

Timeline Model (app/Models/Timeline.php):

  • active() scope - filters status = 'active'
  • user_id foreign key
  • updates() - HasMany relationship to TimelineUpdate

TimelineUpdate Model (app/Models/TimelineUpdate.php):

  • timeline() - BelongsTo relationship to Timeline
  • update_text column
  • created_at timestamp

Acceptance Criteria

Welcome Section

  • Display "Welcome, {client name}" greeting
  • Use localized greeting based on user's preferred language
  • Show current date in user's locale format

Upcoming Consultations Widget

  • Display next approved consultation (or "No upcoming consultations" if none)
  • Show consultation date/time formatted per locale (AR: DD/MM/YYYY, EN: MM/DD/YYYY)
  • Show time in 12-hour format (AM/PM)
  • Display type badge: "Free" (green) or "Paid" (gold)
  • Display status badge with appropriate color
  • "View Details" link to consultation details (Story 7.2)

Active Cases Widget

  • Display count of active timelines
  • Show preview of most recent update (truncated to ~100 chars)
  • "View All Cases" link to timelines list (Story 7.3)
  • Empty state: "No active cases" with muted styling

Recent Updates Widget

  • Display last 3 timeline updates across all user's cases
  • Each update shows: case name, update date, preview text
  • "View Timeline" link for each update
  • Empty state: "No recent updates"

Booking Status Widget

  • Display count of pending booking requests
  • Show booking limit indicator:
    • Can book: "You can book a consultation today" (green)
    • Cannot book: "You already have a booking for today" (amber)
  • "Book Consultation" button linking to booking page (Story 7.5)
  • Disable button if daily limit reached

Design Requirements

  • Card-based layout using Flux UI components
  • Color scheme: Navy (#0A1F44) background sections, Gold (#D4AF37) accents
  • Mobile-first responsive (stack cards vertically on mobile)
  • All text content bilingual (Arabic RTL / English LTR)
  • Use consistent spacing: gap-6 between cards, p-6 card padding

Edge Cases & Empty States

  • No consultations: Show empty state with "Book your first consultation" CTA
  • No timelines: Show empty state "No cases assigned yet"
  • No updates: Show empty state "No recent updates"
  • Loading state: Show skeleton loaders while data fetches

Technical Implementation

Files to Create/Modify

File Action Purpose
resources/views/livewire/client/dashboard.blade.php Create Main Volt component
routes/web.php Modify Add client dashboard route
resources/views/components/layouts/client.blade.php Create/Verify Client layout if not exists

Route Configuration

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

Component Structure

<?php

use App\Models\TimelineUpdate;
use Livewire\Volt\Component;

new class extends Component {
    public function with(): array
    {
        $user = auth()->user();

        return [
            'upcomingConsultation' => $user->consultations()
                ->approved()
                ->upcoming()
                ->orderBy('scheduled_date')
                ->orderBy('scheduled_time')
                ->first(),
            'activeTimelinesCount' => $user->timelines()->active()->count(),
            'recentUpdates' => TimelineUpdate::whereHas('timeline', fn($q) => $q->where('user_id', $user->id))
                ->latest()
                ->take(3)
                ->with('timeline')
                ->get(),
            'pendingBookingsCount' => $user->consultations()->pending()->count(),
            'canBookToday' => !$user->consultations()
                ->whereDate('scheduled_date', today())
                ->whereIn('status', ['pending', 'approved'])
                ->exists(),
        ];
    }
}; ?>

<div>
    {{-- Welcome Section --}}
    {{-- Widgets Grid --}}
</div>

Flux UI Components to Use

  • <flux:card> - Widget containers
  • <flux:heading> - Section titles
  • <flux:badge> - Status/type indicators
  • <flux:button> - CTAs
  • <flux:text> - Body content
  • <flux:skeleton> - Loading states

Localization

  • Create/update resources/lang/en/client.php and resources/lang/ar/client.php
  • Keys needed: dashboard.welcome, dashboard.upcoming, dashboard.cases, dashboard.updates, dashboard.booking, empty state messages

Testing Requirements

Test File

tests/Feature/Client/DashboardTest.php

Test Scenarios

use App\Models\User;
use App\Models\Consultation;
use App\Models\Timeline;
use App\Models\TimelineUpdate;
use Livewire\Volt\Volt;

test('client can view their dashboard', function () {
    $user = User::factory()->create();

    $this->actingAs($user)
        ->get(route('client.dashboard'))
        ->assertOk()
        ->assertSeeLivewire('client.dashboard');
});

test('dashboard shows only authenticated user consultations', function () {
    $user = User::factory()->create();
    $otherUser = User::factory()->create();

    $myConsultation = Consultation::factory()->approved()->upcoming()->for($user)->create();
    $otherConsultation = Consultation::factory()->approved()->upcoming()->for($otherUser)->create();

    Volt::test('client.dashboard')
        ->actingAs($user)
        ->assertSee($myConsultation->scheduled_date->format('...'))
        ->assertDontSee($otherConsultation->scheduled_date->format('...'));
});

test('dashboard shows correct active timelines count', function () {
    $user = User::factory()->create();
    Timeline::factory()->active()->count(3)->for($user)->create();
    Timeline::factory()->archived()->count(2)->for($user)->create();

    Volt::test('client.dashboard')
        ->actingAs($user)
        ->assertSee('3'); // Only active count
});

test('dashboard shows last 3 timeline updates', function () {
    $user = User::factory()->create();
    $timeline = Timeline::factory()->for($user)->create();
    TimelineUpdate::factory()->count(5)->for($timeline)->create();

    Volt::test('client.dashboard')
        ->actingAs($user)
        ->assertViewHas('recentUpdates', fn($updates) => $updates->count() === 3);
});

test('canBookToday is false when user has booking today', function () {
    $user = User::factory()->create();
    Consultation::factory()->approved()->for($user)->create([
        'scheduled_date' => today(),
    ]);

    Volt::test('client.dashboard')
        ->actingAs($user)
        ->assertSet('canBookToday', false);
});

test('canBookToday is true when user has no booking today', function () {
    $user = User::factory()->create();

    Volt::test('client.dashboard')
        ->actingAs($user)
        ->assertSet('canBookToday', true);
});

test('dashboard handles empty state gracefully', function () {
    $user = User::factory()->create();

    Volt::test('client.dashboard')
        ->actingAs($user)
        ->assertSee(__('client.dashboard.no_upcoming'))
        ->assertSee(__('client.dashboard.no_cases'));
});

test('unauthenticated user cannot access dashboard', function () {
    $this->get(route('client.dashboard'))
        ->assertRedirect(route('login'));
});

Factory Requirements

Ensure factories exist with states:

  • Consultation::factory()->approved(), ->pending(), ->upcoming()
  • Timeline::factory()->active(), ->archived()

References

PRD Sections

  • Section 5.8 - Client Dashboard requirements and components
  • Section 7.1 - Design requirements (color scheme, typography)
  • Section 5.4 - Booking rules (1 per day limit, consultation types)
  • Section 5.5 - Timeline system structure

Design Specifications

  • Primary: Navy Blue #0A1F44
  • Accent: Gold #D4AF37
  • Card styling: shadow-sm, rounded-lg, border border-gray-200
  • Spacing scale per PRD Section 7.1
  • Story 7.2: My Consultations View (link target for upcoming consultation)
  • Story 7.3: My Cases/Timelines View (link target for cases widget)
  • Story 7.5: New Booking Interface (link target for book button)

Definition of Done

  • Volt component created at resources/views/livewire/client/dashboard.blade.php
  • Route registered and protected with auth middleware
  • All 5 widgets display correctly with real data
  • Data strictly scoped to authenticated user (security verified)
  • Empty states display appropriately for each widget
  • Mobile responsive (tested on 375px viewport)
  • Bilingual content working (AR/EN toggle)
  • All test scenarios pass
  • Code formatted with vendor/bin/pint --dirty
  • No console errors or warnings

Estimation

Complexity: Medium | Effort: 4-5 hours

Out of Scope

  • Consultation detail view (Story 7.2)
  • Timeline detail view (Story 7.3)
  • Booking form functionality (Story 7.5)
  • Real-time updates via websockets