7.2 KiB
7.2 KiB
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
- Technology: Livewire Volt with pagination
- Follows pattern: Admin list pattern
- Touch points: Post CRUD operations
Acceptance Criteria
List View
- Display all posts with:
- Title (in current admin language)
- Status (draft/published)
- Created date
- Last updated date
- Pagination (10/25/50 per page)
Filtering & Search
- Filter by status (draft/published/all)
- Search by title or body content
- Sort by date (newest/oldest)
Quick Actions
- Edit post
- Delete (with confirmation)
- Publish/unpublish toggle
- Bulk delete option (optional)
Quality Requirements
- Bilingual labels
- Audit log for delete actions
- Tests for list operations
Technical Notes
Volt Component
<?php
use App\Models\Post;
use Livewire\Volt\Component;
use Livewire\WithPagination;
new class extends Component {
use WithPagination;
public string $search = '';
public string $statusFilter = '';
public string $sortBy = 'created_at';
public string $sortDir = 'desc';
public int $perPage = 10;
public function updatedSearch()
{
$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 = '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
<div>
<div class="flex justify-between items-center mb-6">
<flux:heading>{{ __('admin.posts') }}</flux:heading>
<flux:button href="{{ route('admin.posts.create') }}">
{{ __('admin.create_post') }}
</flux:button>
</div>
<!-- Filters -->
<div class="flex gap-4 mb-4">
<flux:input
wire:model.live.debounce="search"
placeholder="{{ __('admin.search_posts') }}"
class="w-64"
/>
<flux:select wire:model.live="statusFilter">
<option value="">{{ __('admin.all_statuses') }}</option>
<option value="draft">{{ __('admin.draft') }}</option>
<option value="published">{{ __('admin.published') }}</option>
</flux:select>
</div>
<!-- Posts Table -->
<table class="w-full">
<thead>
<tr>
<th wire:click="sort('title_{{ app()->getLocale() }}')" class="cursor-pointer">
{{ __('admin.title') }}
</th>
<th>{{ __('admin.status') }}</th>
<th wire:click="sort('created_at')" class="cursor-pointer">
{{ __('admin.created') }}
</th>
<th wire:click="sort('updated_at')" class="cursor-pointer">
{{ __('admin.updated') }}
</th>
<th>{{ __('admin.actions') }}</th>
</tr>
</thead>
<tbody>
@forelse($posts as $post)
<tr>
<td>{{ $post->title }}</td>
<td>
<flux:badge :variant="$post->status === 'published' ? 'success' : 'secondary'">
{{ __('admin.' . $post->status) }}
</flux:badge>
</td>
<td>{{ $post->created_at->format('d/m/Y') }}</td>
<td>{{ $post->updated_at->diffForHumans() }}</td>
<td>
<div class="flex gap-2">
<flux:button size="sm" href="{{ route('admin.posts.edit', $post) }}">
{{ __('admin.edit') }}
</flux:button>
<flux:button
size="sm"
wire:click="togglePublish({{ $post->id }})"
>
{{ $post->status === 'published' ? __('admin.unpublish') : __('admin.publish') }}
</flux:button>
<flux:button
size="sm"
variant="danger"
wire:click="delete({{ $post->id }})"
wire:confirm="{{ __('admin.confirm_delete_post') }}"
>
{{ __('admin.delete') }}
</flux:button>
</div>
</td>
</tr>
@empty
<tr>
<td colspan="5" class="text-center py-8 text-charcoal/70">
{{ __('admin.no_posts') }}
</td>
</tr>
@endforelse
</tbody>
</table>
{{ $posts->links() }}
</div>
Definition of Done
- List displays all posts
- Filter by status works
- Search by title/body works
- Sort by date works
- Quick publish/unpublish toggle works
- Delete with confirmation works
- Pagination works
- Audit log for actions
- Tests pass
- Code formatted with Pint
Dependencies
- Story 5.1: Post creation
Estimation
Complexity: Medium Estimated Effort: 3-4 hours