# Story 5.2: Post Management Dashboard ## Epic Reference **Epic 5:** Posts/Blog System ## User Story As an **admin**, I want **a dashboard to manage all blog posts**, So that **I can organize, publish, and maintain content efficiently**. ## Story Context ### Existing System Integration - **Integrates with:** posts table, admin_logs table - **Technology:** Livewire Volt with pagination - **Follows pattern:** Admin list pattern (see similar components in `resources/views/livewire/admin/`) - **Touch points:** Post CRUD operations, AdminLog audit trail - **Note:** HTML sanitization uses `clean()` helper (from mews/purifier package, configured in Story 5.1) ## Acceptance Criteria ### List View - [x] Display all posts with: - Title (in current admin language) - Status (draft/published) - Created date - Last updated date - [x] Pagination (10/25/50 per page) ### Filtering & Search - [x] Filter by status (draft/published/all) - [x] Search by title or body content - [x] Sort by date (newest/oldest) ### Quick Actions - [x] Edit post - [x] Delete (with confirmation) - [x] Publish/unpublish toggle - [ ] Bulk delete option (optional) - **OUT OF SCOPE** ### Quality Requirements - [x] Bilingual labels - [x] Audit log for delete and status change actions - [x] Authorization check (admin only) on all actions - [x] Tests for list operations ### Edge Cases - [x] Handle post not found gracefully (race condition on delete) - [x] Per-page selector visible in UI (10/25/50 options) - [x] Bulk delete is **out of scope** for this story (marked optional) ## Technical Notes ### File Location Create Volt component at: `resources/views/livewire/admin/posts/index.blade.php` ### Route ```php // routes/web.php (admin group) Route::get('/admin/posts', function () { return view('livewire.admin.posts.index'); })->middleware(['auth', 'admin'])->name('admin.posts.index'); ``` ### Volt Component ```php resetPage(); } public function sort(string $column): void { if ($this->sortBy === $column) { $this->sortDir = $this->sortDir === 'asc' ? 'desc' : 'asc'; } else { $this->sortBy = $column; $this->sortDir = 'desc'; } } public function togglePublish(int $id): void { $post = Post::findOrFail($id); $newStatus = $post->status === 'published' ? 'draft' : 'published'; $post->update(['status' => $newStatus]); AdminLog::create([ 'admin_id' => auth()->id(), 'action_type' => 'status_change', 'target_type' => 'post', 'target_id' => $post->id, 'old_values' => ['status' => $post->getOriginal('status')], 'new_values' => ['status' => $newStatus], 'ip_address' => request()->ip(), ]); session()->flash('success', __('messages.post_status_updated')); } public function delete(int $id): void { $post = Post::findOrFail($id); AdminLog::create([ 'admin_id' => auth()->id(), 'action_type' => 'delete', 'target_type' => 'post', 'target_id' => $post->id, 'old_values' => $post->toArray(), 'ip_address' => request()->ip(), ]); $post->delete(); session()->flash('success', __('messages.post_deleted')); } public function with(): array { $locale = app()->getLocale(); return [ 'posts' => Post::query() ->when($this->search, fn($q) => $q->where(function($q) use ($locale) { $q->where("title_{$locale}", 'like', "%{$this->search}%") ->orWhere("body_{$locale}", 'like', "%{$this->search}%"); })) ->when($this->statusFilter, fn($q) => $q->where('status', $this->statusFilter)) ->orderBy($this->sortBy, $this->sortDir) ->paginate($this->perPage), ]; } }; ``` ### Template ```blade
{{ __('admin.posts') }} {{ __('admin.create_post') }}
@forelse($posts as $post) @empty @endforelse
{{ __('admin.title') }} {{ __('admin.status') }} {{ __('admin.created') }} {{ __('admin.updated') }} {{ __('admin.actions') }}
{{ $post->title }} {{ __('admin.' . $post->status) }} {{ $post->created_at->format('d/m/Y') }} {{ $post->updated_at->diffForHumans() }}
{{ __('admin.edit') }} {{ $post->status === 'published' ? __('admin.unpublish') : __('admin.publish') }} {{ __('admin.delete') }}
{{ __('admin.no_posts') }}
{{ $posts->links() }}
``` ## Testing Requirements ### Test Scenarios ```php // tests/Feature/Admin/PostManagementTest.php test('admin can view posts list', function () { // Create admin, create posts, visit index, assert sees posts }); test('admin can filter posts by status', function () { // Create draft and published posts // Filter by 'draft' - only drafts visible // Filter by 'published' - only published visible // Filter by '' (all) - all visible }); test('admin can search posts by title in current locale', function () { // Create posts with known titles // Search by partial title - matches appear // Search with no matches - empty state shown }); test('admin can search posts by body content', function () { // Create posts with known body content // Search by body text - matches appear }); test('admin can sort posts by date', function () { // Create posts with different dates // Sort desc - newest first // Sort asc - oldest first }); test('admin can toggle post publish status', function () { // Create draft post // Toggle - becomes published, AdminLog created // Toggle again - becomes draft, AdminLog created }); test('admin can delete post with confirmation', function () { // Create post // Delete - post removed, AdminLog created }); test('pagination works correctly', function () { // Create 15 posts // Page 1 shows 10, page 2 shows 5 }); test('audit log records status changes', function () { // Toggle status, verify AdminLog entry with old/new values }); test('audit log records deletions', function () { // Delete post, verify AdminLog entry with old values }); ``` ## Definition of Done - [x] List displays all posts - [x] Filter by status works - [x] Search by title/body works - [x] Sort by date works - [x] Quick publish/unpublish toggle works - [x] Delete with confirmation works - [x] Pagination works - [x] Per-page selector works (10/25/50) - [x] Audit log for actions - [x] All test scenarios pass - [x] Code formatted with Pint ## Dependencies - **Story 5.1:** Post creation - `docs/stories/story-5.1-post-creation-editing.md` - **Requires:** Post model with bilingual accessors, AdminLog model ## Estimation **Complexity:** Medium **Estimated Effort:** 3-4 hours --- ## Dev Agent Record ### Status **Ready for Review** ### Agent Model Used Claude Opus 4.5 (claude-opus-4-5-20251101) ### File List | File | Action | |------|--------| | `resources/views/livewire/admin/posts/index.blade.php` | Modified - Added togglePublish, delete methods, body search, per-page 10/25/50 | | `lang/en/posts.php` | Modified - Added post_status_updated, post_not_found, confirm_delete keys | | `lang/ar/posts.php` | Modified - Added post_status_updated, post_not_found, confirm_delete keys | | `tests/Feature/Admin/PostManagementTest.php` | Modified - Added 13 new tests for index page features | ### Completion Notes - Post management dashboard index page was already partially implemented from Story 5.1 - Added missing features: togglePublish, delete with confirmation, body search, audit logging - Changed per-page default from 15 to 10 and options from 15/25/50 to 10/25/50 per story requirements - Uses DB transaction with lockForUpdate for race condition protection - All 41 post management tests pass (13 new tests added for index page features) - Full test suite (682 tests) passes - Code formatted with Pint ### Change Log | Date | Change | |------|--------| | 2025-12-27 | Initial implementation of Story 5.2 features | ## QA Results ### Review Date: 2025-12-27 ### Reviewed By: Quinn (Test Architect) ### Risk Assessment - **Risk Level**: Low-Medium - **Escalation Triggers**: None triggered - No auth/payment/security-sensitive changes (post management is admin-only) - Tests added: 13 new tests for index page features - Diff size: Reasonable (~300 lines of Livewire component + tests) - Previous gate: N/A (first review) - Acceptance criteria count: 9 items (moderate complexity) ### Code Quality Assessment **Overall Grade: Excellent** The implementation demonstrates high-quality Laravel/Livewire practices: 1. **Architecture & Design**: Clean Volt class-based component following project conventions 2. **Database Safety**: Proper use of `DB::transaction()` with `lockForUpdate()` for race condition protection 3. **Error Handling**: Graceful handling of "post not found" scenarios with user-friendly error messages 4. **Bilingual Support**: Search properly queries both Arabic and English JSON fields 5. **State Management**: Proper use of `updatedX()` lifecycle hooks for pagination reset 6. **Code Organization**: Clear separation of concerns between filtering, sorting, and CRUD operations ### Refactoring Performed None required. The code is well-structured and follows project conventions. ### Requirements Traceability (Given-When-Then Mapping) | AC# | Acceptance Criteria | Test Coverage | Status | |-----|---------------------|---------------|--------| | 1 | Display posts with title, status, dates | `admin can see list of posts`, `posts list shows status badges` | ✓ | | 2 | Pagination (10/25/50 per page) | `pagination works correctly with per page selector` | ✓ | | 3 | Filter by status | `admin can filter posts by status` | ✓ | | 4 | Search by title/body | `admin can search posts by title`, `admin can search posts by body content` | ✓ | | 5 | Sort by date | `admin can sort posts by created date`, `admin can sort posts by updated date` | ✓ | | 6 | Edit post | `admin can view post edit form`, `edit existing post updates content` | ✓ | | 7 | Delete with confirmation | `admin can delete post from index`, `delete handles post not found gracefully` | ✓ | | 8 | Publish/unpublish toggle | `admin can toggle post publish status from draft to published`, `...from published to draft` | ✓ | | 9 | Audit logging | `toggle publish creates audit log`, `delete post creates audit log` | ✓ | | 10 | Authorization (admin only) | `non-admin cannot access posts index`, `guest cannot access posts index` | ✓ | ### Compliance Check - Coding Standards: ✓ Code formatted with Pint - Project Structure: ✓ Follows Volt class-based component pattern in correct location - Testing Strategy: ✓ Comprehensive feature tests using Pest with Volt::test() - All ACs Met: ✓ All acceptance criteria have corresponding test coverage ### Improvements Checklist All items handled - no required changes: - [x] DB transactions with lockForUpdate() for race condition protection - [x] Graceful error handling for "not found" scenarios - [x] Bilingual search across all JSON fields - [x] Pagination reset on filter changes - [x] Audit logging for status changes and deletions - [x] Authorization via admin middleware - [x] Per-page selector with 10/25/50 options per story requirements ### Test Architecture Assessment **Tests Reviewed**: 41 tests (13 specific to index page features) **Coverage Quality**: Excellent | Category | Count | Quality | |----------|-------|---------| | Index page CRUD | 16 | ✓ Complete | | Authorization | 4 | ✓ Complete | | Validation | 2 | ✓ Complete | | Create/Edit pages | 15 | ✓ Complete | | Model tests | 4 | ✓ Complete | **Test Design Strengths**: - Uses factory states (`->draft()`, `->published()`) appropriately - Tests both happy paths and edge cases (not found scenarios) - Verifies audit log creation with correct old/new values - Tests all filter/sort combinations ### Security Review - ✓ Authorization: Admin middleware protects all routes - ✓ Input Validation: Search input used in parameterized queries (no SQL injection) - ✓ XSS Prevention: HTML sanitization via `clean()` helper (mews/purifier) - ✓ CSRF: Handled by Livewire automatically - ✓ Race Conditions: Protected with `lockForUpdate()` in transactions ### Performance Considerations - ✓ Pagination implemented with configurable per-page limit - ✓ JSON search uses MySQL JSON_EXTRACT (appropriate for moderate data volumes) - ✓ Sorting uses indexed columns (updated_at, created_at) - Note: For very large datasets, consider adding full-text search index on JSON columns ### Non-Functional Requirements Validation | NFR | Status | Notes | |-----|--------|-------| | Security | PASS | Admin-only access, parameterized queries, XSS protection | | Performance | PASS | Pagination, indexed sorting columns | | Reliability | PASS | Transaction protection, graceful error handling | | Maintainability | PASS | Clean code, comprehensive tests, bilingual support | ### Files Modified During Review None - no refactoring was needed. ### Gate Status Gate: **PASS** → docs/qa/gates/5.2-post-management-dashboard.yml ### Recommended Status ✓ **Ready for Done** All acceptance criteria are met, tests pass, code quality is excellent, and security considerations are properly addressed. The implementation follows Laravel/Livewire best practices and project conventions.