268 lines
9.5 KiB
Markdown
268 lines
9.5 KiB
Markdown
# Story 6.1: Dashboard Overview & Statistics
|
|
|
|
## Epic Reference
|
|
**Epic 6:** Admin Dashboard
|
|
|
|
## User Story
|
|
As an **admin**,
|
|
I want **to see real-time metrics and key statistics at a glance**,
|
|
So that **I can understand the current state of my practice**.
|
|
|
|
## Prerequisites / Dependencies
|
|
|
|
This story requires the following to be completed first:
|
|
|
|
| Dependency | Required From | What's Needed |
|
|
|------------|---------------|---------------|
|
|
| User Model | Epic 2 | `status` (active/deactivated), `user_type` (individual/company) fields |
|
|
| Consultation Model | Epic 3 | `consultations` table with `status`, `consultation_type`, `scheduled_date` |
|
|
| Timeline Model | Epic 4 | `timelines` table with `status` (active/archived) |
|
|
| Timeline Updates | Epic 4 | `timeline_updates` table with `created_at` |
|
|
| Post Model | Epic 5 | `posts` table with `status` (published/draft), `created_at` |
|
|
| Admin Layout | Epic 1 | Admin authenticated layout with navigation |
|
|
|
|
**References:**
|
|
- Epic 6 details: `docs/epics/epic-6-admin-dashboard.md`
|
|
- PRD Dashboard Section: `docs/prd.md` Section 5.7 (Admin Dashboard)
|
|
- Database Schema: `docs/prd.md` Section 16.1
|
|
|
|
## Acceptance Criteria
|
|
|
|
### User Metrics Card
|
|
- [ ] Total active clients (individual + company with status = active)
|
|
- [ ] Individual vs company breakdown (count each type)
|
|
- [ ] Deactivated clients count
|
|
- [ ] New clients this month (created_at in current month)
|
|
|
|
### Booking Metrics Card
|
|
- [ ] Pending requests count (highlighted with warning color)
|
|
- [ ] Today's consultations (scheduled for today, approved status)
|
|
- [ ] This week's consultations
|
|
- [ ] This month's consultations
|
|
- [ ] Free vs paid breakdown (consultation_type field)
|
|
- [ ] No-show rate percentage (no-show / total completed * 100)
|
|
|
|
### Timeline Metrics Card
|
|
- [ ] Active case timelines (status = active)
|
|
- [ ] Archived timelines (status = archived)
|
|
- [ ] Updates added this week (timeline_updates created in last 7 days)
|
|
|
|
### Posts Metrics Card
|
|
- [ ] Total published posts (status = published)
|
|
- [ ] Posts published this month
|
|
|
|
### Design
|
|
- [ ] Clean card-based layout using Flux UI components
|
|
- [ ] Color-coded status indicators (gold for highlights, success green, warning colors)
|
|
- [ ] Responsive grid (2 columns on tablet, 1 on mobile, 4 on desktop)
|
|
- [ ] Navy blue and gold color scheme per PRD Section 7.1
|
|
|
|
## Technical Implementation
|
|
|
|
### Files to Create/Modify
|
|
|
|
| File | Purpose |
|
|
|------|---------|
|
|
| `resources/views/livewire/admin/dashboard.blade.php` | Main Volt component |
|
|
| `routes/web.php` | Add admin dashboard route |
|
|
|
|
### Route Definition
|
|
```php
|
|
Route::middleware(['auth', 'admin'])->prefix('admin')->group(function () {
|
|
Route::get('/dashboard', function () {
|
|
return view('livewire.admin.dashboard');
|
|
})->name('admin.dashboard');
|
|
});
|
|
```
|
|
|
|
### Component Structure (Volt Class-Based)
|
|
|
|
```php
|
|
<?php
|
|
|
|
use App\Models\User;
|
|
use App\Models\Consultation;
|
|
use App\Models\Timeline;
|
|
use App\Models\TimelineUpdate;
|
|
use App\Models\Post;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Livewire\Volt\Component;
|
|
|
|
new class extends Component {
|
|
public function with(): array
|
|
{
|
|
return [
|
|
'userMetrics' => $this->getUserMetrics(),
|
|
'bookingMetrics' => $this->getBookingMetrics(),
|
|
'timelineMetrics' => $this->getTimelineMetrics(),
|
|
'postMetrics' => $this->getPostMetrics(),
|
|
];
|
|
}
|
|
|
|
private function getUserMetrics(): array
|
|
{
|
|
return Cache::remember('admin.metrics.users', 300, fn() => [
|
|
'total_active' => User::where('status', 'active')
|
|
->whereIn('user_type', ['individual', 'company'])->count(),
|
|
'individual' => User::where('user_type', 'individual')
|
|
->where('status', 'active')->count(),
|
|
'company' => User::where('user_type', 'company')
|
|
->where('status', 'active')->count(),
|
|
'deactivated' => User::where('status', 'deactivated')->count(),
|
|
'new_this_month' => User::whereMonth('created_at', now()->month)
|
|
->whereYear('created_at', now()->year)->count(),
|
|
]);
|
|
}
|
|
|
|
private function getBookingMetrics(): array
|
|
{
|
|
return Cache::remember('admin.metrics.bookings', 300, function () {
|
|
$total = Consultation::whereIn('status', ['completed', 'no-show'])->count();
|
|
$noShows = Consultation::where('status', 'no-show')->count();
|
|
|
|
return [
|
|
'pending' => Consultation::where('status', 'pending')->count(),
|
|
'today' => Consultation::whereDate('scheduled_date', today())
|
|
->where('status', 'approved')->count(),
|
|
'this_week' => Consultation::whereBetween('scheduled_date', [
|
|
now()->startOfWeek(), now()->endOfWeek()
|
|
])->count(),
|
|
'this_month' => Consultation::whereMonth('scheduled_date', now()->month)
|
|
->whereYear('scheduled_date', now()->year)->count(),
|
|
'free' => Consultation::where('consultation_type', 'free')->count(),
|
|
'paid' => Consultation::where('consultation_type', 'paid')->count(),
|
|
'no_show_rate' => $total > 0 ? round(($noShows / $total) * 100, 1) : 0,
|
|
];
|
|
});
|
|
}
|
|
|
|
private function getTimelineMetrics(): array
|
|
{
|
|
return Cache::remember('admin.metrics.timelines', 300, fn() => [
|
|
'active' => Timeline::where('status', 'active')->count(),
|
|
'archived' => Timeline::where('status', 'archived')->count(),
|
|
'updates_this_week' => TimelineUpdate::where('created_at', '>=', now()->subWeek())->count(),
|
|
]);
|
|
}
|
|
|
|
private function getPostMetrics(): array
|
|
{
|
|
return Cache::remember('admin.metrics.posts', 300, fn() => [
|
|
'total_published' => Post::where('status', 'published')->count(),
|
|
'this_month' => Post::where('status', 'published')
|
|
->whereMonth('created_at', now()->month)
|
|
->whereYear('created_at', now()->year)->count(),
|
|
]);
|
|
}
|
|
}; ?>
|
|
|
|
<div>
|
|
{{-- Dashboard content with Flux UI cards --}}
|
|
</div>
|
|
```
|
|
|
|
### Flux UI Components to Use
|
|
- `<flux:heading>` - Page title
|
|
- `<flux:card>` or custom card component - Metric cards (if available, otherwise Tailwind)
|
|
- `<flux:badge>` - Status indicators
|
|
- `<flux:text>` - Metric labels and values
|
|
|
|
### Cache Strategy
|
|
- **TTL:** 300 seconds (5 minutes) for all metrics
|
|
- **Keys:** `admin.metrics.users`, `admin.metrics.bookings`, `admin.metrics.timelines`, `admin.metrics.posts`
|
|
- **Invalidation:** Consider cache clearing when data changes (optional enhancement)
|
|
|
|
## Edge Cases & Error Handling
|
|
|
|
| Scenario | Expected Behavior |
|
|
|----------|-------------------|
|
|
| Empty database (0 clients) | All metrics display "0" - no errors |
|
|
| No consultations exist | No-show rate displays "0%" (not "N/A" or error) |
|
|
| New month (1st day) | "This month" metrics show 0 |
|
|
| Cache failure | Queries execute directly without caching (graceful degradation) |
|
|
| Division by zero (no-show rate) | Return 0 when total consultations is 0 |
|
|
|
|
## Testing Requirements
|
|
|
|
### Test File
|
|
`tests/Feature/Admin/DashboardTest.php`
|
|
|
|
### Test Scenarios
|
|
|
|
```php
|
|
// 1. Dashboard loads successfully for admin
|
|
test('admin can view dashboard', function () {
|
|
$admin = User::factory()->admin()->create();
|
|
|
|
$this->actingAs($admin)
|
|
->get(route('admin.dashboard'))
|
|
->assertSuccessful()
|
|
->assertSee('Dashboard');
|
|
});
|
|
|
|
// 2. Metrics display correctly with sample data
|
|
test('dashboard displays correct user metrics', function () {
|
|
User::factory()->count(5)->create(['status' => 'active', 'user_type' => 'individual']);
|
|
User::factory()->count(3)->create(['status' => 'active', 'user_type' => 'company']);
|
|
User::factory()->count(2)->create(['status' => 'deactivated']);
|
|
|
|
// Assert metrics show 8 active, 5 individual, 3 company, 2 deactivated
|
|
});
|
|
|
|
// 3. Empty state handling
|
|
test('dashboard handles empty database gracefully', function () {
|
|
$admin = User::factory()->admin()->create();
|
|
|
|
$this->actingAs($admin)
|
|
->get(route('admin.dashboard'))
|
|
->assertSuccessful()
|
|
->assertSee('0'); // Should show zeros, not errors
|
|
});
|
|
|
|
// 4. No-show rate calculation
|
|
test('no-show rate calculates correctly', function () {
|
|
// Create 10 completed, 2 no-shows = 20% rate
|
|
});
|
|
|
|
// 5. Cache behavior
|
|
test('metrics are cached for 5 minutes', function () {
|
|
// Verify cache key exists after first load
|
|
});
|
|
|
|
// 6. Non-admin cannot access
|
|
test('non-admin cannot access dashboard', function () {
|
|
$client = User::factory()->client()->create();
|
|
|
|
$this->actingAs($client)
|
|
->get(route('admin.dashboard'))
|
|
->assertForbidden();
|
|
});
|
|
```
|
|
|
|
### Manual Testing Checklist
|
|
- [ ] Verify responsive layout on mobile (375px)
|
|
- [ ] Verify responsive layout on tablet (768px)
|
|
- [ ] Verify responsive layout on desktop (1200px+)
|
|
- [ ] Verify pending count is highlighted/prominent
|
|
- [ ] Verify color scheme matches PRD (navy blue, gold)
|
|
- [ ] Verify RTL layout works correctly (Arabic)
|
|
|
|
## Definition of Done
|
|
- [ ] All metric cards display correctly with accurate data
|
|
- [ ] Data is cached with 5-minute TTL
|
|
- [ ] Empty states handled gracefully (show 0, no errors)
|
|
- [ ] No-show rate handles division by zero
|
|
- [ ] Responsive layout works on mobile, tablet, desktop
|
|
- [ ] Color scheme matches brand guidelines
|
|
- [ ] All tests pass
|
|
- [ ] Admin-only access enforced
|
|
- [ ] Code formatted with Pint
|
|
|
|
## Estimation
|
|
**Complexity:** Medium | **Effort:** 4-5 hours
|
|
|
|
## Out of Scope
|
|
- Real-time updates (polling/websockets) - covered in Story 6.3
|
|
- Charts and visualizations - covered in Story 6.2
|
|
- Quick action buttons - covered in Story 6.3
|