# Story 4.4: Admin Timeline Dashboard ## Epic Reference **Epic 4:** Case Timeline System ## User Story As an **admin**, I want **a central view to manage all timelines across all clients**, So that **I can efficiently track and update case progress**. ## Story Context ### Existing System Integration - **Integrates with:** timelines table, users table - **Technology:** Livewire Volt with pagination - **Follows pattern:** Admin list/dashboard pattern - **Touch points:** All timeline operations - **Authorization:** Admin middleware protects route (defined in `routes/web.php`) ### 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` - Relationships: `belongsTo(User::class)` as `user`, `hasMany(TimelineUpdate::class)` as `updates` - Database schema: ```php // timelines table $table->foreignId('user_id')->constrained()->cascadeOnDelete(); $table->string('case_name'); $table->string('case_reference')->nullable()->unique(); $table->enum('status', ['active', 'archived'])->default('active'); ``` **From Story 4.3 (`docs/stories/story-4.3-timeline-archiving.md`):** - Timeline model has `archive()`, `unarchive()`, `isArchived()` methods - Scopes available: `scopeActive()`, `scopeArchived()` - AdminLog is created on status changes ## Acceptance Criteria ### List View - [ ] Display all timelines with: - Case name - Client name - Status (active/archived) - Last update date - Update count - [ ] Pagination (15/25/50 per page) - [ ] Empty state message when no timelines exist ### Filtering - [ ] Filter by client (search/select) - [ ] Filter by status (active/archived/all) - [ ] Filter by date range (created/updated) - [ ] Search by case name or reference - [ ] Clear filters option - [ ] Show "No results" message when filters return empty ### Sorting - [ ] Sort by client name - [ ] Sort by case name - [ ] Sort by last updated - [ ] Sort by created date - [ ] Visual indicator for current sort column/direction ### Quick Actions - [ ] View timeline details - [ ] Add update (inline or link) - [ ] Archive/unarchive toggle with confirmation ### Quality Requirements - [ ] Fast loading with eager loading - [ ] Bilingual support - [ ] Tests for filtering/sorting ## Technical Notes ### File Location `resources/views/livewire/admin/timelines/index.blade.php` ### Route Definition ```php // routes/web.php (within admin middleware group) Route::get('/admin/timelines', \App\Livewire\Admin\Timelines\Index::class) ->name('admin.timelines.index'); ``` ### Volt Component ```php resetPage(); } public function updatedClientFilter(): void { $this->resetPage(); } public function updatedStatusFilter(): void { $this->resetPage(); } public function sort(string $column): void { if ($this->sortBy === $column) { $this->sortDir = $this->sortDir === 'asc' ? 'desc' : 'asc'; } else { $this->sortBy = $column; $this->sortDir = 'asc'; } } public function toggleArchive(int $id): void { $timeline = Timeline::findOrFail($id); if ($timeline->isArchived()) { $timeline->unarchive(); $action = 'unarchive'; $message = __('messages.timeline_unarchived'); } else { $timeline->archive(); $action = 'archive'; $message = __('messages.timeline_archived'); } AdminLog::create([ 'admin_id' => auth()->id(), 'action_type' => $action, 'target_type' => 'timeline', 'target_id' => $timeline->id, 'ip_address' => request()->ip(), ]); session()->flash('success', $message); } public function clearFilters(): void { $this->reset(['search', 'clientFilter', 'statusFilter', 'dateFrom', 'dateTo']); $this->resetPage(); } public function with(): array { return [ 'timelines' => Timeline::query() ->with(['user', 'updates' => fn($q) => $q->latest()->limit(1)]) ->withCount('updates') ->when($this->search, fn($q) => $q->where(function($q) { $q->where('case_name', 'like', "%{$this->search}%") ->orWhere('case_reference', 'like', "%{$this->search}%"); })) ->when($this->clientFilter, fn($q) => $q->where('user_id', $this->clientFilter)) ->when($this->statusFilter, fn($q) => $q->where('status', $this->statusFilter)) ->when($this->dateFrom, fn($q) => $q->where('created_at', '>=', $this->dateFrom)) ->when($this->dateTo, fn($q) => $q->where('created_at', '<=', $this->dateTo)) ->orderBy($this->sortBy, $this->sortDir) ->paginate($this->perPage), 'clients' => \App\Models\User::query() ->whereHas('timelines') ->orderBy('name') ->get(['id', 'name']), ]; } }; ``` ### Template Structure ```blade
@foreach($clients as $client) @endforeach {{ __('admin.clear_filters') }}
@if($timelines->isEmpty())
{{ __('admin.no_timelines_found') }}
@else @foreach($timelines as $timeline) @endforeach
{{ __('admin.case_name') }} @if($sortBy === 'case_name') @endif {{ __('admin.client') }} {{ __('admin.status') }} {{ __('admin.last_update') }} @if($sortBy === 'updated_at') @endif {{ __('admin.updates') }} {{ __('admin.actions') }}
{{ $timeline->case_name }} {{ $timeline->user->name }} {{ __('admin.' . $timeline->status) }} {{ $timeline->updated_at->diffForHumans() }} {{ $timeline->updates_count }} {{ __('admin.actions') }} {{ __('admin.view') }} {{ __('admin.add_update') }} {{ $timeline->status === 'active' ? __('admin.archive') : __('admin.unarchive') }}
{{ $timelines->links() }}
@endif
``` ## Test Scenarios ### Feature Tests Create test file: `tests/Feature/Admin/TimelineDashboardTest.php` ```php use App\Models\{User, Timeline}; use Livewire\Volt\Volt; // List View Tests test('admin can view timeline dashboard', function () { $admin = User::factory()->admin()->create(); Timeline::factory()->count(5)->create(); $this->actingAs($admin) ->get(route('admin.timelines.index')) ->assertOk() ->assertSeeLivewire('admin.timelines.index'); }); test('timeline dashboard displays all timelines with correct data', function () { $admin = User::factory()->admin()->create(); $timeline = Timeline::factory()->create(['case_name' => 'Test Case']); Volt::test('admin.timelines.index') ->actingAs($admin) ->assertSee('Test Case') ->assertSee($timeline->user->name); }); test('dashboard shows empty state when no timelines exist', function () { $admin = User::factory()->admin()->create(); Volt::test('admin.timelines.index') ->actingAs($admin) ->assertSee(__('admin.no_timelines_found')); }); // Filtering Tests test('can filter timelines by status', function () { $admin = User::factory()->admin()->create(); $active = Timeline::factory()->create(['status' => 'active', 'case_name' => 'Active Case']); $archived = Timeline::factory()->create(['status' => 'archived', 'case_name' => 'Archived Case']); Volt::test('admin.timelines.index') ->actingAs($admin) ->set('statusFilter', 'active') ->assertSee('Active Case') ->assertDontSee('Archived Case'); }); test('can filter timelines by client', function () { $admin = User::factory()->admin()->create(); $client1 = User::factory()->create(); $client2 = User::factory()->create(); Timeline::factory()->create(['user_id' => $client1->id, 'case_name' => 'Client1 Case']); Timeline::factory()->create(['user_id' => $client2->id, 'case_name' => 'Client2 Case']); Volt::test('admin.timelines.index') ->actingAs($admin) ->set('clientFilter', $client1->id) ->assertSee('Client1 Case') ->assertDontSee('Client2 Case'); }); test('can search timelines by case name', function () { $admin = User::factory()->admin()->create(); Timeline::factory()->create(['case_name' => 'Contract Dispute']); Timeline::factory()->create(['case_name' => 'Property Issue']); Volt::test('admin.timelines.index') ->actingAs($admin) ->set('search', 'Contract') ->assertSee('Contract Dispute') ->assertDontSee('Property Issue'); }); test('can search timelines by case reference', function () { $admin = User::factory()->admin()->create(); Timeline::factory()->create(['case_reference' => 'REF-123', 'case_name' => 'Case A']); Timeline::factory()->create(['case_reference' => 'REF-456', 'case_name' => 'Case B']); Volt::test('admin.timelines.index') ->actingAs($admin) ->set('search', 'REF-123') ->assertSee('Case A') ->assertDontSee('Case B'); }); test('clear filters resets all filter values', function () { $admin = User::factory()->admin()->create(); Volt::test('admin.timelines.index') ->actingAs($admin) ->set('search', 'test') ->set('statusFilter', 'active') ->call('clearFilters') ->assertSet('search', '') ->assertSet('statusFilter', ''); }); // Sorting Tests test('can sort timelines by case name', function () { $admin = User::factory()->admin()->create(); Timeline::factory()->create(['case_name' => 'Zebra Case']); Timeline::factory()->create(['case_name' => 'Alpha Case']); Volt::test('admin.timelines.index') ->actingAs($admin) ->call('sort', 'case_name') ->assertSet('sortBy', 'case_name') ->assertSet('sortDir', 'asc'); }); test('clicking same sort column toggles direction', function () { $admin = User::factory()->admin()->create(); Volt::test('admin.timelines.index') ->actingAs($admin) ->call('sort', 'case_name') ->assertSet('sortDir', 'asc') ->call('sort', 'case_name') ->assertSet('sortDir', 'desc'); }); // Quick Actions Tests test('can archive timeline from dashboard', function () { $admin = User::factory()->admin()->create(); $timeline = Timeline::factory()->create(['status' => 'active']); Volt::test('admin.timelines.index') ->actingAs($admin) ->call('toggleArchive', $timeline->id); expect($timeline->fresh()->status)->toBe('archived'); }); test('can unarchive timeline from dashboard', function () { $admin = User::factory()->admin()->create(); $timeline = Timeline::factory()->create(['status' => 'archived']); Volt::test('admin.timelines.index') ->actingAs($admin) ->call('toggleArchive', $timeline->id); expect($timeline->fresh()->status)->toBe('active'); }); test('toggle archive creates admin log entry', function () { $admin = User::factory()->admin()->create(); $timeline = Timeline::factory()->create(['status' => 'active']); Volt::test('admin.timelines.index') ->actingAs($admin) ->call('toggleArchive', $timeline->id); $this->assertDatabaseHas('admin_logs', [ 'admin_id' => $admin->id, 'action_type' => 'archive', 'target_type' => 'timeline', 'target_id' => $timeline->id, ]); }); // Pagination Tests test('pagination displays correct number of items', function () { $admin = User::factory()->admin()->create(); Timeline::factory()->count(20)->create(); Volt::test('admin.timelines.index') ->actingAs($admin) ->assertSet('perPage', 15); }); test('can change items per page', function () { $admin = User::factory()->admin()->create(); Volt::test('admin.timelines.index') ->actingAs($admin) ->set('perPage', 25) ->assertSet('perPage', 25); }); // N+1 Query Prevention Test test('dashboard uses eager loading to prevent N+1 queries', function () { $admin = User::factory()->admin()->create(); Timeline::factory()->count(10)->create(); // This test verifies the query count stays reasonable // The component should make ~3-4 queries regardless of timeline count $this->actingAs($admin); Volt::test('admin.timelines.index'); // If N+1 exists, this would be 10+ queries // With eager loading, should be ~4 queries (timelines, users, updates, clients) }); // Authorization Tests test('non-admin cannot access timeline dashboard', function () { $client = User::factory()->create(); // Regular client $this->actingAs($client) ->get(route('admin.timelines.index')) ->assertForbidden(); }); test('guest cannot access timeline dashboard', function () { $this->get(route('admin.timelines.index')) ->assertRedirect(route('login')); }); ``` ## Definition of Done - [ ] List displays all timelines - [ ] All filters working (status, client, search, date range) - [ ] Clear filters button resets all filters - [ ] All sorts working with visual indicators - [ ] Quick actions functional (view, add update, archive/unarchive) - [ ] Archive/unarchive has confirmation dialog - [ ] Pagination working with per-page selector - [ ] Empty state displayed when no results - [ ] No N+1 queries (verified with eager loading) - [ ] Bilingual support for all labels - [ ] All tests pass - [ ] Code formatted with Pint ## Dependencies - **Story 4.1:** Timeline creation (`docs/stories/story-4.1-timeline-creation.md`) - Timeline model and relationships - **Story 4.3:** Archive functionality (`docs/stories/story-4.3-timeline-archiving.md`) - Archive/unarchive methods on Timeline model ## Estimation **Complexity:** Medium **Estimated Effort:** 3-4 hours