# Story 4.5: Client Timeline View ## Epic Reference **Epic 4:** Case Timeline System ## User Story As a **client**, I want **to view my case timelines and updates**, So that **I can track the progress of my legal matters**. ## Story Context ### Existing System Integration - **Integrates with:** timelines, timeline_updates tables - **Technology:** Livewire Volt (read-only) - **Follows pattern:** Client dashboard pattern - **Touch points:** Client portal navigation - **Authorization:** Client middleware protects routes (defined in `routes/web.php`) ### Relationship to Story 7.3 This story (4.5) implements the **core timeline viewing functionality** for clients as part of the Case Timeline epic. Story 7.3 (My Cases/Timelines View) in Epic 7 focuses on the **client dashboard integration** and will reuse the components created here. Implement this story first; Story 7.3 will integrate these components into the dashboard layout. ### Prerequisites from Previous Stories **From Story 4.1 (`docs/stories/story-4.1-timeline-creation.md`):** - `Timeline` model exists with fields: `user_id`, `case_name`, `case_reference`, `status` - Database schema: ```php Schema::create('timelines', function (Blueprint $table) { $table->id(); $table->foreignId('user_id')->constrained()->cascadeOnDelete(); $table->string('case_name'); $table->string('case_reference')->nullable()->unique(); $table->enum('status', ['active', 'archived'])->default('active'); $table->timestamps(); }); ``` **From Story 4.2 (`docs/stories/story-4.2-timeline-updates-management.md`):** - `TimelineUpdate` model with fields: `timeline_id`, `admin_id`, `update_text` - Timeline has `updates()` HasMany relationship **From Story 4.3 (`docs/stories/story-4.3-timeline-archiving.md`):** - Timeline model has scopes: `scopeActive()`, `scopeArchived()` - Timeline model has methods: `isArchived()` **User Model Requirement:** The `User` model must have: ```php public function timelines(): HasMany { return $this->hasMany(Timeline::class); } ``` ## Acceptance Criteria ### Timeline List - [ ] Display all client's timelines - [ ] Active timelines prominently displayed - [ ] Archived timelines clearly separated - [ ] Visual distinction (color/icon) for status - [ ] Show for each: - Case name and reference - Status indicator - Last update date - Update count ### Individual Timeline View - [ ] Case name and reference - [ ] Status indicator - [ ] All updates in chronological order - [ ] Each update shows: - Date and time - Update content (formatted) ### Restrictions - [ ] Read-only (no edit/comment) - [ ] No ability to archive/delete - [ ] Only see own timelines (403 for unauthorized access) ### UX Features - [ ] Recent updates indicator (new since last view, optional) - [ ] Responsive design for mobile - [ ] Bilingual labels and dates ## Technical Notes ### File Structure ``` Routes (add to routes/web.php within client middleware group): GET /client/timelines -> client.timelines.index GET /client/timelines/{timeline} -> client.timelines.show Files to Create: resources/views/livewire/pages/client/timelines/index.blade.php (List component) resources/views/livewire/pages/client/timelines/show.blade.php (Detail component) Tests: tests/Feature/Client/TimelineViewTest.php ``` ### Route Definition ```php // routes/web.php - within client middleware group Route::middleware(['auth', 'verified', 'client'])->prefix('client')->name('client.')->group(function () { Route::get('/timelines', function () { return view('livewire.pages.client.timelines.index'); })->name('timelines.index'); Route::get('/timelines/{timeline}', function (Timeline $timeline) { return view('livewire.pages.client.timelines.show', ['timeline' => $timeline]); })->name('timelines.show'); }); ``` ### Volt Component for List ```php auth()->user() ->timelines() ->active() ->withCount('updates') ->with(['updates' => fn($q) => $q->latest()->limit(1)]) ->latest('updated_at') ->get(), 'archivedTimelines' => auth()->user() ->timelines() ->archived() ->withCount('updates') ->latest('updated_at') ->get(), ]; } }; ?>
{{ __('client.my_cases') }} {{-- Active Timelines --}} @if($activeTimelines->isNotEmpty())

{{ __('client.active_cases') }}

@foreach($activeTimelines as $timeline)

{{ $timeline->case_name }}

@if($timeline->case_reference)

{{ __('client.reference') }}: {{ $timeline->case_reference }}

@endif

{{ __('client.updates') }}: {{ $timeline->updates_count }} @if($timeline->updates->first()) · {{ __('client.last_update') }}: {{ $timeline->updates->first()->created_at->diffForHumans() }} @endif

{{ __('client.active') }} {{ __('client.view') }}
@endforeach
@endif {{-- Archived Timelines --}} @if($archivedTimelines->isNotEmpty())

{{ __('client.archived_cases') }}

@foreach($archivedTimelines as $timeline)

{{ $timeline->case_name }}

@if($timeline->case_reference)

{{ __('client.reference') }}: {{ $timeline->case_reference }}

@endif

{{ __('client.updates') }}: {{ $timeline->updates_count }}

{{ __('client.archived') }} {{ __('client.view') }}
@endforeach
@endif {{-- Empty State --}} @if($activeTimelines->isEmpty() && $archivedTimelines->isEmpty())

{{ __('client.no_cases_yet') }}

@endif
``` ### Timeline Detail View ```php user_id === auth()->id(), 403); $this->timeline = $timeline->load(['updates' => fn($q) => $q->oldest()]); } }; ?>
{{ $timeline->case_name }} @if($timeline->case_reference)

{{ __('client.reference') }}: {{ $timeline->case_reference }}

@endif
{{ __('client.' . $timeline->status) }}
@forelse($timeline->updates as $update)
{{ $update->created_at->translatedFormat('l, d M Y - g:i A') }}
{!! $update->update_text !!}
@empty

{{ __('client.no_updates_yet') }}

@endforelse
{{ __('client.back_to_cases') }}
``` ### Required Translation Keys ```php // resources/lang/en/client.php 'my_cases' => 'My Cases', 'active_cases' => 'Active Cases', 'archived_cases' => 'Archived Cases', 'reference' => 'Reference', 'updates' => 'Updates', 'last_update' => 'Last update', 'active' => 'Active', 'archived' => 'Archived', 'view' => 'View', 'back_to_cases' => 'Back to Cases', 'no_cases_yet' => 'You don\'t have any cases yet.', 'no_updates_yet' => 'No updates yet.', // resources/lang/ar/client.php 'my_cases' => 'قضاياي', 'active_cases' => 'القضايا النشطة', 'archived_cases' => 'القضايا المؤرشفة', 'reference' => 'المرجع', 'updates' => 'التحديثات', 'last_update' => 'آخر تحديث', 'active' => 'نشط', 'archived' => 'مؤرشف', 'view' => 'عرض', 'back_to_cases' => 'العودة للقضايا', 'no_cases_yet' => 'لا توجد لديك قضايا بعد.', 'no_updates_yet' => 'لا توجد تحديثات بعد.', ``` ## Test Scenarios All tests should use Pest and be placed in `tests/Feature/Client/TimelineViewTest.php`. ```php create(['user_type' => 'individual']); Timeline::factory()->count(3)->create(['user_id' => $client->id]); $this->actingAs($client) ->get(route('client.timelines.index')) ->assertOk() ->assertSeeLivewire('pages.client.timelines.index'); }); test('client cannot view other clients timelines in list', function () { $client = User::factory()->create(['user_type' => 'individual']); $otherClient = User::factory()->create(['user_type' => 'individual']); $otherTimeline = Timeline::factory()->create([ 'user_id' => $otherClient->id, 'case_name' => 'Other Client Case', ]); Volt::test('pages.client.timelines.index') ->actingAs($client) ->assertDontSee('Other Client Case'); }); test('client can view own timeline detail', function () { $client = User::factory()->create(['user_type' => 'individual']); $timeline = Timeline::factory()->create([ 'user_id' => $client->id, 'case_name' => 'My Contract Case', ]); $this->actingAs($client) ->get(route('client.timelines.show', $timeline)) ->assertOk() ->assertSee('My Contract Case'); }); test('client cannot view other clients timeline detail', function () { $client = User::factory()->create(['user_type' => 'individual']); $otherClient = User::factory()->create(['user_type' => 'individual']); $otherTimeline = Timeline::factory()->create(['user_id' => $otherClient->id]); $this->actingAs($client) ->get(route('client.timelines.show', $otherTimeline)) ->assertForbidden(); }); test('guest cannot access timelines', function () { $this->get(route('client.timelines.index')) ->assertRedirect(route('login')); }); test('admin cannot access client timeline routes', function () { $admin = User::factory()->admin()->create(); $this->actingAs($admin) ->get(route('client.timelines.index')) ->assertForbidden(); }); // List View Tests test('active timelines displayed separately from archived', function () { $client = User::factory()->create(['user_type' => 'individual']); Timeline::factory()->create([ 'user_id' => $client->id, 'case_name' => 'Active Case', 'status' => 'active', ]); Timeline::factory()->create([ 'user_id' => $client->id, 'case_name' => 'Archived Case', 'status' => 'archived', ]); Volt::test('pages.client.timelines.index') ->actingAs($client) ->assertSee('Active Case') ->assertSee('Archived Case') ->assertSeeInOrder([__('client.active_cases'), 'Active Case', __('client.archived_cases'), 'Archived Case']); }); test('timeline list shows update count', function () { $client = User::factory()->create(['user_type' => 'individual']); $timeline = Timeline::factory()->create(['user_id' => $client->id]); TimelineUpdate::factory()->count(5)->create(['timeline_id' => $timeline->id]); Volt::test('pages.client.timelines.index') ->actingAs($client) ->assertSee('5'); }); test('empty state shown when no timelines', function () { $client = User::factory()->create(['user_type' => 'individual']); Volt::test('pages.client.timelines.index') ->actingAs($client) ->assertSee(__('client.no_cases_yet')); }); // Detail View Tests test('timeline detail shows all updates chronologically', function () { $client = User::factory()->create(['user_type' => 'individual']); $timeline = Timeline::factory()->create(['user_id' => $client->id]); $oldUpdate = TimelineUpdate::factory()->create([ 'timeline_id' => $timeline->id, 'update_text' => 'First Update', 'created_at' => now()->subDays(2), ]); $newUpdate = TimelineUpdate::factory()->create([ 'timeline_id' => $timeline->id, 'update_text' => 'Second Update', 'created_at' => now()->subDay(), ]); Volt::test('pages.client.timelines.show', ['timeline' => $timeline]) ->actingAs($client) ->assertSeeInOrder(['First Update', 'Second Update']); }); test('timeline detail shows case name and reference', function () { $client = User::factory()->create(['user_type' => 'individual']); $timeline = Timeline::factory()->create([ 'user_id' => $client->id, 'case_name' => 'Property Dispute', 'case_reference' => 'REF-2024-001', ]); Volt::test('pages.client.timelines.show', ['timeline' => $timeline]) ->actingAs($client) ->assertSee('Property Dispute') ->assertSee('REF-2024-001'); }); test('timeline detail shows status badge', function () { $client = User::factory()->create(['user_type' => 'individual']); $activeTimeline = Timeline::factory()->create([ 'user_id' => $client->id, 'status' => 'active', ]); Volt::test('pages.client.timelines.show', ['timeline' => $activeTimeline]) ->actingAs($client) ->assertSee(__('client.active')); }); test('empty updates shows no updates message', function () { $client = User::factory()->create(['user_type' => 'individual']); $timeline = Timeline::factory()->create(['user_id' => $client->id]); Volt::test('pages.client.timelines.show', ['timeline' => $timeline]) ->actingAs($client) ->assertSee(__('client.no_updates_yet')); }); // Read-Only Enforcement Tests test('client cannot edit timeline or updates', function () { $client = User::factory()->create(['user_type' => 'individual']); $timeline = Timeline::factory()->create(['user_id' => $client->id]); // Verify no edit methods exist on the component Volt::test('pages.client.timelines.show', ['timeline' => $timeline]) ->actingAs($client) ->assertMethodDoesNotExist('edit') ->assertMethodDoesNotExist('update') ->assertMethodDoesNotExist('delete') ->assertMethodDoesNotExist('archive'); }); // N+1 Query Prevention Test test('timeline list uses eager loading', function () { $client = User::factory()->create(['user_type' => 'individual']); Timeline::factory()->count(10)->create(['user_id' => $client->id]); // Component should load with reasonable query count $this->actingAs($client); Volt::test('pages.client.timelines.index'); // With eager loading: ~3-4 queries (timelines, updates count, latest update) }); ``` ## Definition of Done - [ ] Volt components created at specified file locations - [ ] Routes registered for client timeline views - [ ] Client can view list of their timelines - [ ] Active/archived clearly separated with visual distinction - [ ] Can view individual timeline details - [ ] All updates displayed chronologically (oldest first) - [ ] Read-only enforced (no edit/delete methods) - [ ] Cannot view other clients' timelines (403 response) - [ ] Empty state displayed when no timelines - [ ] Mobile responsive - [ ] RTL support with proper positioning - [ ] All translation keys added (AR/EN) - [ ] All tests pass - [ ] Code formatted with Pint ## Dependencies - **Story 4.1:** Timeline creation (`docs/stories/story-4.1-timeline-creation.md`) - Timeline model and database - **Story 4.2:** Timeline updates (`docs/stories/story-4.2-timeline-updates-management.md`) - TimelineUpdate model - **Story 4.3:** Timeline archiving (`docs/stories/story-4.3-timeline-archiving.md`) - Active/archived scopes - **Story 7.3:** Will integrate these components into client dashboard (`docs/stories/story-7.3-my-cases-timelines-view.md`) ## Estimation **Complexity:** Medium **Estimated Effort:** 3-4 hours