complete story 7.1 with qa tests

This commit is contained in:
Naser Mansour 2025-12-28 23:17:54 +02:00
parent 47f9caf762
commit 22cdca77bd
8 changed files with 773 additions and 19 deletions

View File

@ -0,0 +1,51 @@
# Quality Gate: Story 7.1 - Client Dashboard Overview
schema: 1
story: "7.1"
story_title: "Client Dashboard Overview"
gate: PASS
status_reason: "All acceptance criteria implemented and verified by 24 passing tests. Clean code following project standards with proper security boundaries."
reviewer: "Quinn (Test Architect)"
updated: "2025-12-28T00:00:00Z"
waiver: { active: false }
top_issues: []
risk_summary:
totals: { critical: 0, high: 0, medium: 0, low: 1 }
recommendations:
must_fix: []
monitor:
- "Consider extracting reorder()->latest() pattern to model method for cleaner reuse"
quality_score: 100
expires: "2026-01-11T00:00:00Z"
evidence:
tests_reviewed: 24
risks_identified: 0
trace:
ac_covered: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
ac_gaps: []
nfr_validation:
security:
status: PASS
notes: "Route protected with auth+client middleware, admin blocked, data scoped to user"
performance:
status: PASS
notes: "Efficient queries with limits and eager loading, no N+1 issues"
reliability:
status: PASS
notes: "All empty states handled gracefully"
maintainability:
status: PASS
notes: "Clean Volt component following project standards, well-organized code"
recommendations:
immediate: []
future:
- action: "Extract reorder()->latest() to a dedicated Timeline method like latestUpdate()"
refs: ["resources/views/livewire/client/dashboard.blade.php:122"]
- action: "Add database index on booking_date if performance degrades"
refs: ["database/migrations/*consultations*"]

View File

@ -278,16 +278,16 @@ Ensure factories exist with states:
- 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
- [x] Volt component created at `resources/views/livewire/client/dashboard.blade.php`
- [x] Route registered and protected with auth middleware
- [x] All 5 widgets display correctly with real data
- [x] Data strictly scoped to authenticated user (security verified)
- [x] Empty states display appropriately for each widget
- [x] Mobile responsive (tested on 375px viewport)
- [x] Bilingual content working (AR/EN toggle)
- [x] All test scenarios pass
- [x] Code formatted with `vendor/bin/pint --dirty`
- [x] No console errors or warnings
## Estimation
**Complexity:** Medium | **Effort:** 4-5 hours
@ -297,3 +297,100 @@ Ensure factories exist with states:
- Timeline detail view (Story 7.3)
- Booking form functionality (Story 7.5)
- Real-time updates via websockets
---
## QA Results
### Review Date: 2025-12-28
### Reviewed By: Quinn (Test Architect)
### Code Quality Assessment
The implementation is well-structured and follows project coding standards. The Volt component uses the class-based pattern correctly, queries are efficient with proper scoping to the authenticated user, and all Flux UI components are used appropriately. The bilingual support is complete with both English and Arabic translations. The component demonstrates good practices with proper separation of concerns in the `with()` method.
### Refactoring Performed
None required - the code quality is already satisfactory.
### Compliance Check
- Coding Standards: ✓ Class-based Volt pattern, Flux UI components, proper localization
- Project Structure: ✓ Files placed in correct locations, routes properly configured
- Testing Strategy: ✓ 24 comprehensive Pest tests with Volt::test() pattern
- All ACs Met: ✓ All acceptance criteria implemented and tested
### Improvements Checklist
- [x] All data scoped to authenticated user (verified via tests)
- [x] Empty states display appropriately for all widgets
- [x] Mobile responsive layout with card grid
- [x] Bilingual content working (AR/EN)
- [x] All 24 tests passing
- [x] Code formatted with pint --dirty
- [ ] Consider extracting the `reorder()->latest()` pattern in blade (line 122) to a dedicated model method or scope for cleaner reuse
### Security Review
No security concerns found:
- Route protected with `auth` and `client` middleware
- Admin users properly blocked (403 response verified by test)
- All data queries scoped to authenticated user
- No SQL injection risks (Eloquent used throughout)
- Proper authorization boundaries between clients
### Performance Considerations
No significant performance concerns:
- Queries are efficient with proper indexing on foreign keys
- `take(3)` limits recent updates query
- Eager loading used for timeline relationship on updates
- Consider adding database index on `booking_date` if consultation queries become slow (future optimization)
### Files Modified During Review
None - no modifications made.
### Gate Status
Gate: PASS → docs/qa/gates/7.1-client-dashboard-overview.yml
### Recommended Status
✓ Ready for Done - All acceptance criteria met, comprehensive test coverage, clean implementation.
---
## Dev Agent Record
### Status
**Ready for Review**
### Agent Model Used
Claude Opus 4.5
### File List
| File | Action | Purpose |
|------|--------|---------|
| `resources/views/livewire/client/dashboard.blade.php` | Created | Main client dashboard Volt component with 5 widgets |
| `routes/web.php` | Modified | Changed client dashboard route to use Volt::route() |
| `lang/en/client.php` | Modified | Added dashboard localization keys |
| `lang/ar/client.php` | Modified | Added Arabic dashboard localization keys |
| `tests/Feature/Client/DashboardTest.php` | Created | 24 feature tests for client dashboard |
| `resources/views/livewire/client/dashboard-placeholder.blade.php` | Deleted | Removed placeholder file |
### Change Log
- Created client dashboard Volt component with Welcome Section, Upcoming Consultation Widget, Active Cases Widget, Recent Updates Widget, and Booking Status Widget
- Updated route from `Route::view()` to `Volt::route()` for proper Livewire component rendering
- Added 18 localization keys for dashboard UI in both English and Arabic
- Implemented proper data scoping to authenticated user only
- Used existing `x-layouts.app` layout (no need for separate client layout)
- Fixed query ordering issue with `reorder()` for timeline updates relationship
- All 24 tests pass covering authorization, data display, empty states, and booking status logic
### Completion Notes
- Skeleton loaders not implemented (Livewire handles loading states automatically via wire:loading)
- Consultation model uses `booking_date`/`booking_time` columns (not `scheduled_date`/`scheduled_time` as in story spec)
- Client layout reuses existing `app.blade.php` layout which supports both admin and client contexts

View File

@ -1,6 +1,28 @@
<?php
return [
// Dashboard
'dashboard' => [
'title' => 'لوحة التحكم',
'welcome' => 'مرحباً، :name',
'upcoming_consultation' => 'الاستشارة القادمة',
'view_details' => 'عرض التفاصيل',
'no_upcoming' => 'لا توجد استشارات قادمة',
'book_first' => 'احجز استشارتك الأولى',
'active_cases' => 'القضايا النشطة',
'cases_count' => '{1} قضية نشطة|[2,10] قضايا نشطة|[11,*] قضية نشطة',
'latest_update' => 'آخر تحديث',
'view_all_cases' => 'عرض جميع القضايا',
'no_cases' => 'لا توجد قضايا مسندة بعد',
'recent_updates' => 'التحديثات الأخيرة',
'no_updates' => 'لا توجد تحديثات حديثة',
'booking_status' => 'حالة الحجز',
'pending_bookings' => '{1} :count طلب حجز معلق|[2,10] :count طلبات حجز معلقة|[11,*] :count طلب حجز معلق',
'can_book' => 'يمكنك حجز استشارة اليوم',
'cannot_book' => 'لديك حجز بالفعل لهذا اليوم',
'book_consultation' => 'حجز استشارة',
],
// Timeline views
'my_cases' => 'قضاياي',
'active_cases' => 'القضايا النشطة',

View File

@ -1,6 +1,28 @@
<?php
return [
// Dashboard
'dashboard' => [
'title' => 'Dashboard',
'welcome' => 'Welcome, :name',
'upcoming_consultation' => 'Upcoming Consultation',
'view_details' => 'View Details',
'no_upcoming' => 'No upcoming consultations',
'book_first' => 'Book your first consultation',
'active_cases' => 'Active Cases',
'cases_count' => '{1} active case|[2,*] active cases',
'latest_update' => 'Latest update',
'view_all_cases' => 'View All Cases',
'no_cases' => 'No cases assigned yet',
'recent_updates' => 'Recent Updates',
'no_updates' => 'No recent updates',
'booking_status' => 'Booking Status',
'pending_bookings' => '{1} :count pending booking request|[2,*] :count pending booking requests',
'can_book' => 'You can book a consultation today',
'cannot_book' => 'You already have a booking for today',
'book_consultation' => 'Book Consultation',
],
// Timeline views
'my_cases' => 'My Cases',
'active_cases' => 'Active Cases',

View File

@ -1,8 +0,0 @@
<x-layouts.app>
<div class="flex items-center justify-center min-h-[60vh]">
<div class="text-center">
<flux:heading size="xl">{{ __('Client Dashboard') }}</flux:heading>
<flux:text class="mt-2 text-zinc-500">{{ __('Dashboard coming soon') }}</flux:text>
</div>
</div>
</x-layouts.app>

View File

@ -0,0 +1,244 @@
<?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()
->where('booking_date', '>=', today())
->orderBy('booking_date')
->orderBy('booking_time')
->first(),
'activeTimelinesCount' => $user->timelines()->active()->count(),
'latestTimeline' => $user->timelines()->active()->latest()->first(),
'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('booking_date', today())
->whereIn('status', ['pending', 'approved'])
->exists(),
];
}
}; ?>
<div class="space-y-6 p-6">
{{-- Welcome Section --}}
<div class="rounded-lg border border-zinc-200 bg-[#0A1F44] p-6 text-white dark:border-zinc-700">
<flux:heading size="xl" class="text-white">
{{ __('client.dashboard.welcome', ['name' => auth()->user()->full_name]) }}
</flux:heading>
<flux:text class="mt-1 text-zinc-300">
{{ now()->locale(app()->getLocale())->translatedFormat(app()->getLocale() === 'ar' ? 'l، j F Y' : 'l, F j, Y') }}
</flux:text>
</div>
{{-- Widgets Grid --}}
<div class="grid gap-6 md:grid-cols-2">
{{-- Upcoming Consultation Widget --}}
<div class="rounded-lg border border-zinc-200 bg-white p-6 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
<flux:heading size="lg" class="mb-4">
{{ __('client.dashboard.upcoming_consultation') }}
</flux:heading>
@if ($upcomingConsultation)
<div class="space-y-3">
<div class="flex items-center gap-2">
<flux:icon name="calendar" class="h-5 w-5 text-zinc-500" />
<flux:text>
@if (app()->getLocale() === 'ar')
{{ $upcomingConsultation->booking_date->format('d/m/Y') }}
@else
{{ $upcomingConsultation->booking_date->format('m/d/Y') }}
@endif
</flux:text>
</div>
<div class="flex items-center gap-2">
<flux:icon name="clock" class="h-5 w-5 text-zinc-500" />
<flux:text>
{{ \Carbon\Carbon::parse($upcomingConsultation->booking_time)->format('g:i A') }}
</flux:text>
</div>
<div class="flex flex-wrap gap-2">
@if ($upcomingConsultation->consultation_type->value === 'free')
<flux:badge color="green">{{ $upcomingConsultation->consultation_type->label() }}</flux:badge>
@else
<flux:badge color="yellow">{{ $upcomingConsultation->consultation_type->label() }}</flux:badge>
@endif
<flux:badge color="sky">{{ $upcomingConsultation->status->label() }}</flux:badge>
</div>
<div class="pt-2">
<flux:button
variant="ghost"
size="sm"
:href="route('client.consultations.index')"
wire:navigate
>
{{ __('client.dashboard.view_details') }}
</flux:button>
</div>
</div>
@else
<div class="text-center py-4">
<flux:icon name="calendar-days" class="mx-auto h-12 w-12 text-zinc-300" />
<flux:text class="mt-2 text-zinc-500">{{ __('client.dashboard.no_upcoming') }}</flux:text>
<div class="mt-4">
<flux:button
variant="primary"
size="sm"
:href="route('client.consultations.book')"
wire:navigate
>
{{ __('client.dashboard.book_first') }}
</flux:button>
</div>
</div>
@endif
</div>
{{-- Active Cases Widget --}}
<div class="rounded-lg border border-zinc-200 bg-white p-6 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
<flux:heading size="lg" class="mb-4">
{{ __('client.dashboard.active_cases') }}
</flux:heading>
@if ($activeTimelinesCount > 0)
<div class="space-y-3">
<div class="flex items-center gap-2">
<span class="text-3xl font-bold text-[#D4AF37]">{{ $activeTimelinesCount }}</span>
<flux:text class="text-zinc-500">{{ trans_choice('client.dashboard.cases_count', $activeTimelinesCount) }}</flux:text>
</div>
@if ($latestTimeline)
@php
$latestUpdate = $latestTimeline->updates()->reorder()->latest()->first();
@endphp
@if ($latestUpdate)
<div class="rounded-lg bg-zinc-50 p-3 dark:bg-zinc-800">
<flux:text size="sm" class="text-zinc-600 dark:text-zinc-400">
{{ __('client.dashboard.latest_update') }}:
</flux:text>
<flux:text class="mt-1">
{{ Str::limit($latestUpdate->update_text, 100) }}
</flux:text>
</div>
@endif
@endif
<div class="pt-2">
<flux:button
variant="ghost"
size="sm"
:href="route('client.timelines.index')"
wire:navigate
>
{{ __('client.dashboard.view_all_cases') }}
</flux:button>
</div>
</div>
@else
<div class="text-center py-4">
<flux:icon name="folder-open" class="mx-auto h-12 w-12 text-zinc-300" />
<flux:text class="mt-2 text-zinc-500">{{ __('client.dashboard.no_cases') }}</flux:text>
</div>
@endif
</div>
{{-- Recent Updates Widget --}}
<div class="rounded-lg border border-zinc-200 bg-white p-6 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
<flux:heading size="lg" class="mb-4">
{{ __('client.dashboard.recent_updates') }}
</flux:heading>
@if ($recentUpdates->isNotEmpty())
<div class="space-y-3">
@foreach ($recentUpdates as $update)
<div class="border-b border-zinc-100 pb-3 last:border-0 last:pb-0 dark:border-zinc-800">
<div class="flex items-start justify-between gap-2">
<div class="flex-1">
<flux:text size="sm" class="font-medium">
{{ $update->timeline->case_name }}
</flux:text>
<flux:text size="sm" class="text-zinc-500">
{{ $update->created_at->locale(app()->getLocale())->diffForHumans() }}
</flux:text>
<flux:text class="mt-1">
{{ Str::limit($update->update_text, 80) }}
</flux:text>
</div>
<flux:button
variant="ghost"
size="xs"
:href="route('client.timelines.show', $update->timeline)"
wire:navigate
icon="arrow-right"
/>
</div>
</div>
@endforeach
</div>
@else
<div class="text-center py-4">
<flux:icon name="bell-slash" class="mx-auto h-12 w-12 text-zinc-300" />
<flux:text class="mt-2 text-zinc-500">{{ __('client.dashboard.no_updates') }}</flux:text>
</div>
@endif
</div>
{{-- Booking Status Widget --}}
<div class="rounded-lg border border-zinc-200 bg-white p-6 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
<flux:heading size="lg" class="mb-4">
{{ __('client.dashboard.booking_status') }}
</flux:heading>
<div class="space-y-4">
@if ($pendingBookingsCount > 0)
<div class="flex items-center gap-2">
<flux:icon name="clock" class="h-5 w-5 text-amber-500" />
<flux:text>
{{ trans_choice('client.dashboard.pending_bookings', $pendingBookingsCount, ['count' => $pendingBookingsCount]) }}
</flux:text>
</div>
@endif
<div class="rounded-lg p-3 {{ $canBookToday ? 'bg-green-50 dark:bg-green-900/20' : 'bg-amber-50 dark:bg-amber-900/20' }}">
@if ($canBookToday)
<div class="flex items-center gap-2">
<flux:icon name="check-circle" class="h-5 w-5 text-green-600" />
<flux:text class="text-green-700 dark:text-green-400">
{{ __('client.dashboard.can_book') }}
</flux:text>
</div>
@else
<div class="flex items-center gap-2">
<flux:icon name="information-circle" class="h-5 w-5 text-amber-600" />
<flux:text class="text-amber-700 dark:text-amber-400">
{{ __('client.dashboard.cannot_book') }}
</flux:text>
</div>
@endif
</div>
<div class="pt-2">
<flux:button
variant="primary"
:href="route('client.consultations.book')"
wire:navigate
:disabled="!$canBookToday"
class="w-full justify-center"
>
{{ __('client.dashboard.book_consultation') }}
</flux:button>
</div>
</div>
</div>
</div>
</div>

View File

@ -125,7 +125,7 @@ Route::middleware(['auth', 'active'])->group(function () {
// Client routes
Route::middleware('client')->prefix('client')->name('client.')->group(function () {
Route::view('/dashboard', 'livewire.client.dashboard-placeholder')
Volt::route('/dashboard', 'client.dashboard')
->name('dashboard');
// Consultations

View File

@ -0,0 +1,326 @@
<?php
use App\Models\Consultation;
use App\Models\Timeline;
use App\Models\TimelineUpdate;
use App\Models\User;
use Livewire\Volt\Volt;
// Authorization Tests
test('client can view their dashboard', function () {
$user = User::factory()->individual()->create();
$this->actingAs($user)
->get(route('client.dashboard'))
->assertOk();
});
test('company client can view dashboard', function () {
$user = User::factory()->company()->create();
$this->actingAs($user)
->get(route('client.dashboard'))
->assertOk();
});
test('unauthenticated user cannot access dashboard', function () {
$this->get(route('client.dashboard'))
->assertRedirect(route('login'));
});
test('admin cannot access client dashboard', function () {
$admin = User::factory()->admin()->create();
$this->actingAs($admin)
->get(route('client.dashboard'))
->assertForbidden();
});
// Welcome Section Tests
test('dashboard shows welcome message with user name', function () {
$user = User::factory()->individual()->create(['full_name' => 'John Doe']);
$this->actingAs($user);
Volt::test('client.dashboard')
->assertSee('John Doe');
});
// Upcoming Consultation Widget Tests
test('dashboard shows upcoming approved consultation', function () {
$user = User::factory()->individual()->create();
$consultation = Consultation::factory()->approved()->create([
'user_id' => $user->id,
'booking_date' => today()->addDays(3),
'booking_time' => '10:00:00',
]);
$this->actingAs($user);
Volt::test('client.dashboard')
->assertSee('10:00 AM');
});
test('dashboard shows only authenticated user consultations', function () {
$user = User::factory()->individual()->create();
$otherUser = User::factory()->individual()->create();
Consultation::factory()->approved()->create([
'user_id' => $user->id,
'booking_date' => today()->addDays(3),
'booking_time' => '09:00:00',
]);
Consultation::factory()->approved()->create([
'user_id' => $otherUser->id,
'booking_date' => today()->addDays(2),
'booking_time' => '14:00:00',
]);
$this->actingAs($user);
Volt::test('client.dashboard')
->assertSee('9:00 AM')
->assertDontSee('2:00 PM');
});
test('dashboard shows no upcoming consultations when none exist', function () {
$user = User::factory()->individual()->create();
$this->actingAs($user);
Volt::test('client.dashboard')
->assertSee(__('client.dashboard.no_upcoming'));
});
test('dashboard does not show past approved consultations', function () {
$user = User::factory()->individual()->create();
Consultation::factory()->approved()->create([
'user_id' => $user->id,
'booking_date' => today()->subDay(),
'booking_time' => '10:00:00',
]);
$this->actingAs($user);
Volt::test('client.dashboard')
->assertSee(__('client.dashboard.no_upcoming'));
});
test('dashboard does not show pending consultations as upcoming', function () {
$user = User::factory()->individual()->create();
Consultation::factory()->pending()->create([
'user_id' => $user->id,
'booking_date' => today()->addDays(3),
'booking_time' => '11:00:00',
]);
$this->actingAs($user);
Volt::test('client.dashboard')
->assertSee(__('client.dashboard.no_upcoming'));
});
// Active Cases Widget Tests
test('dashboard shows correct active timelines count', function () {
$user = User::factory()->individual()->create();
Timeline::factory()->active()->count(3)->create(['user_id' => $user->id]);
Timeline::factory()->archived()->count(2)->create(['user_id' => $user->id]);
$this->actingAs($user);
Volt::test('client.dashboard')
->assertSee('3');
});
test('dashboard shows no cases empty state when user has no timelines', function () {
$user = User::factory()->individual()->create();
$this->actingAs($user);
Volt::test('client.dashboard')
->assertSee(__('client.dashboard.no_cases'));
});
test('dashboard shows latest update preview for active cases', function () {
$user = User::factory()->individual()->create();
$timeline = Timeline::factory()->active()->create(['user_id' => $user->id]);
TimelineUpdate::factory()->create([
'timeline_id' => $timeline->id,
'update_text' => 'This is the latest case update for testing purposes.',
]);
$this->actingAs($user);
Volt::test('client.dashboard')
->assertSee('This is the latest case update');
});
// Recent Updates Widget Tests
test('dashboard shows last 3 timeline updates', function () {
$user = User::factory()->individual()->create();
$timeline = Timeline::factory()->create(['user_id' => $user->id]);
TimelineUpdate::factory()->create([
'timeline_id' => $timeline->id,
'update_text' => 'Update One',
'created_at' => now()->subDays(4),
]);
TimelineUpdate::factory()->create([
'timeline_id' => $timeline->id,
'update_text' => 'Update Two',
'created_at' => now()->subDays(3),
]);
TimelineUpdate::factory()->create([
'timeline_id' => $timeline->id,
'update_text' => 'Update Three',
'created_at' => now()->subDays(2),
]);
TimelineUpdate::factory()->create([
'timeline_id' => $timeline->id,
'update_text' => 'Update Four',
'created_at' => now()->subDay(),
]);
TimelineUpdate::factory()->create([
'timeline_id' => $timeline->id,
'update_text' => 'Update Five',
'created_at' => now(),
]);
$this->actingAs($user);
// Should see the 3 most recent updates, not the older ones
Volt::test('client.dashboard')
->assertSee('Update Five')
->assertSee('Update Four')
->assertSee('Update Three')
->assertDontSee('Update Two')
->assertDontSee('Update One');
});
test('dashboard shows no updates empty state when user has no timeline updates', function () {
$user = User::factory()->individual()->create();
$this->actingAs($user);
Volt::test('client.dashboard')
->assertSee(__('client.dashboard.no_updates'));
});
// Booking Status Widget Tests
test('canBookToday is false when user has approved booking today', function () {
$user = User::factory()->individual()->create();
Consultation::factory()->approved()->create([
'user_id' => $user->id,
'booking_date' => today(),
]);
$this->actingAs($user);
Volt::test('client.dashboard')
->assertSee(__('client.dashboard.cannot_book'));
});
test('canBookToday is false when user has pending booking today', function () {
$user = User::factory()->individual()->create();
Consultation::factory()->pending()->create([
'user_id' => $user->id,
'booking_date' => today(),
]);
$this->actingAs($user);
Volt::test('client.dashboard')
->assertSee(__('client.dashboard.cannot_book'));
});
test('canBookToday is true when user has no booking today', function () {
$user = User::factory()->individual()->create();
$this->actingAs($user);
Volt::test('client.dashboard')
->assertSee(__('client.dashboard.can_book'));
});
test('canBookToday is true when user has only rejected booking today', function () {
$user = User::factory()->individual()->create();
Consultation::factory()->rejected()->create([
'user_id' => $user->id,
'booking_date' => today(),
]);
$this->actingAs($user);
Volt::test('client.dashboard')
->assertSee(__('client.dashboard.can_book'));
});
test('canBookToday is true when user has only cancelled booking today', function () {
$user = User::factory()->individual()->create();
Consultation::factory()->cancelled()->create([
'user_id' => $user->id,
'booking_date' => today(),
]);
$this->actingAs($user);
Volt::test('client.dashboard')
->assertSee(__('client.dashboard.can_book'));
});
test('dashboard shows pending bookings count', function () {
$user = User::factory()->individual()->create();
Consultation::factory()->pending()->count(3)->create([
'user_id' => $user->id,
'booking_date' => today()->addDays(5),
]);
$this->actingAs($user);
Volt::test('client.dashboard')
->assertSee('3');
});
// Empty State Tests
test('dashboard handles empty state gracefully', function () {
$user = User::factory()->individual()->create();
$this->actingAs($user);
Volt::test('client.dashboard')
->assertSee(__('client.dashboard.no_upcoming'))
->assertSee(__('client.dashboard.no_cases'))
->assertSee(__('client.dashboard.no_updates'))
->assertSee(__('client.dashboard.can_book'));
});
// Data Isolation Tests
test('dashboard does not show other users timeline updates', function () {
$user = User::factory()->individual()->create();
$otherUser = User::factory()->individual()->create();
$otherTimeline = Timeline::factory()->create(['user_id' => $otherUser->id]);
TimelineUpdate::factory()->create([
'timeline_id' => $otherTimeline->id,
'update_text' => 'Other user secret update',
]);
$this->actingAs($user);
Volt::test('client.dashboard')
->assertDontSee('Other user secret update');
});
test('dashboard does not count other users active timelines', function () {
$user = User::factory()->individual()->create();
$otherUser = User::factory()->individual()->create();
Timeline::factory()->active()->create(['user_id' => $user->id]);
Timeline::factory()->active()->count(5)->create(['user_id' => $otherUser->id]);
$this->actingAs($user);
// User should only see their 1 active timeline, not the 5 from other user
Volt::test('client.dashboard')
->assertSee('1');
});