generated sotries
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
# 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
|
||||
<?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
|
||||
```blade
|
||||
<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
|
||||
Reference in New Issue
Block a user