# Story 6.3: Quick Actions Panel ## Epic Reference **Epic 6:** Admin Dashboard ## User Story As an **admin**, I want **quick access to pending items and common tasks**, So that **I can efficiently manage my daily workflow without navigating away from the dashboard**. ## Business Context The admin dashboard from Story 6.1 provides static metrics. This story adds actionable widgets that surface items requiring immediate attention (pending bookings, today's schedule) and shortcuts to frequent tasks. This transforms the dashboard from an information display into a productivity hub. ## Prerequisites / Dependencies This story requires the following to be completed first: | Dependency | Required From | What's Needed | |------------|---------------|---------------| | Dashboard Layout | Story 6.1 | Admin dashboard page with card-based layout | | Consultation Model | Epic 3 | `consultations` table with `status`, `scheduled_date`, `scheduled_time` fields | | Consultation Scopes | Epic 3 | `pending()` and `approved()` query scopes on Consultation model | | Timeline Model | Epic 4 | `timelines` table with `user_id`, `case_name` fields | | TimelineUpdate Model | Epic 4 | `timeline_updates` table with `timeline_id`, `created_at` | | User Model | Epic 2 | Client users that consultations reference | | Post Routes | Epic 5 | Route for creating posts (`admin.posts.create`) | | User Routes | Epic 2 | Route for creating users (`admin.users.create`) | **References:** - Story 6.1 Dashboard Layout: `docs/stories/story-6.1-dashboard-overview-statistics.md` - Epic 6 Dashboard Details: `docs/epics/epic-6-admin-dashboard.md` - PRD Dashboard Section: `docs/prd.md` Section 5.7 (Admin Dashboard) ## Acceptance Criteria ### Pending Bookings Widget - [ ] Display count badge showing number of pending consultation requests - [ ] Urgent indicator (red/warning styling) when pending count > 0 - [ ] Mini list showing up to 5 most recent pending bookings with: - Client name - Requested date - Consultation type (free/paid) - [ ] "View All" link navigating to booking management page (`admin.consultations.index`) - [ ] Empty state message when no pending bookings ### Today's Schedule Widget - [ ] List of today's approved consultations ordered by time - [ ] Each item displays: - Scheduled time (formatted for locale) - Client name - Consultation type badge (free/paid) - [ ] Quick status buttons for each consultation: - "Complete" - marks as completed - "No-show" - marks as no-show - [ ] Empty state message when no consultations scheduled today ### Recent Timeline Updates Widget - [ ] Display last 5 timeline updates across all clients - [ ] Each item shows: - Update preview (truncated to ~50 chars) - Case name - Client name - Relative timestamp ("2 hours ago") - [ ] Click navigates to the specific timeline (`admin.timelines.show`) - [ ] Empty state message when no recent updates ### Quick Action Buttons - [ ] **Create User** button - navigates to `admin.users.create` - [ ] **Create Post** button - navigates to `admin.posts.create` - [ ] **Block Time Slot** button - opens modal to block availability - Date picker for selecting date - Time range (start/end time) - Optional reason field - Save creates a "blocked" consultation record ### Notification Bell (Header) - [ ] Bell icon in admin header/navbar - [ ] Badge showing total pending items count (pending bookings) - [ ] Badge hidden when count is 0 - [ ] Click navigates to pending bookings ### Real-time Updates - [ ] Widgets auto-refresh via Livewire polling every 30 seconds - [ ] No full page reload required - [ ] Visual indication during refresh (subtle loading state) ## Technical Implementation ### Files to Create/Modify | File | Purpose | |------|---------| | `resources/views/livewire/admin/dashboard.blade.php` | Add widget sections to existing dashboard | | `resources/views/livewire/admin/widgets/pending-bookings.blade.php` | Pending bookings widget (Volt component) | | `resources/views/livewire/admin/widgets/todays-schedule.blade.php` | Today's schedule widget (Volt component) | | `resources/views/livewire/admin/widgets/recent-updates.blade.php` | Recent timeline updates widget (Volt component) | | `resources/views/livewire/admin/widgets/quick-actions.blade.php` | Quick action buttons widget (Volt component) | | `resources/views/components/layouts/admin.blade.php` | Add notification bell to admin header | ### Widget Architecture Each widget is a separate Volt component for isolation and independent polling: ```php // resources/views/livewire/admin/widgets/pending-bookings.blade.php Consultation::pending()->count(), 'pendingBookings' => Consultation::pending() ->with('user:id,name') ->latest() ->take(5) ->get(), ]; } }; ?>
{{ __('Pending Bookings') }} @if($pendingCount > 0) {{ $pendingCount }} @endif
@forelse($pendingBookings as $booking)
{{ $booking->user->name }}
{{ $booking->scheduled_date->format('M j') }} - {{ $booking->consultation_type }}
@empty {{ __('No pending bookings') }} @endforelse @if($pendingCount > 5) {{ __('View all :count pending', ['count' => $pendingCount]) }} @endif
``` ### Today's Schedule Widget with Actions ```php // resources/views/livewire/admin/widgets/todays-schedule.blade.php Consultation::approved() ->whereDate('scheduled_date', today()) ->with('user:id,name') ->orderBy('scheduled_time') ->get(), ]; } public function markComplete(int $consultationId): void { $consultation = Consultation::findOrFail($consultationId); $consultation->update(['status' => 'completed']); // Optionally dispatch event for logging } public function markNoShow(int $consultationId): void { $consultation = Consultation::findOrFail($consultationId); $consultation->update(['status' => 'no-show']); } }; ?>
{{ __("Today's Schedule") }} @forelse($todaySchedule as $consultation)
{{ \Carbon\Carbon::parse($consultation->scheduled_time)->format('g:i A') }}
{{ $consultation->user->name }}
{{ __('Complete') }} {{ __('No-show') }}
@empty {{ __('No consultations scheduled today') }} @endforelse
``` ### Block Time Slot Modal ```php // In quick-actions.blade.php blockDate = today()->format('Y-m-d'); $this->showBlockModal = true; } public function blockTimeSlot(): void { $this->validate(); Consultation::create([ 'scheduled_date' => $this->blockDate, 'scheduled_time' => $this->blockStartTime, 'end_time' => $this->blockEndTime, 'status' => 'blocked', 'notes' => $this->blockReason, 'user_id' => null, // No client for blocked slots ]); $this->showBlockModal = false; $this->reset(['blockDate', 'blockStartTime', 'blockEndTime', 'blockReason']); } }; ?>
{{ __('Create User') }} {{ __('Create Post') }} {{ __('Block Time Slot') }}
{{ __('Block Time Slot') }}
{{ __('Date') }}
{{ __('Start Time') }} {{ __('End Time') }}
{{ __('Reason (optional)') }}
{{ __('Cancel') }} {{ __('Block Slot') }}
``` ### Dashboard Integration ```php // In resources/views/livewire/admin/dashboard.blade.php // Add after the metrics cards from Story 6.1
{{-- Quick Actions Panel --}}
{{ __('Quick Actions') }}
{{-- Pending Bookings Widget --}}
{{-- Today's Schedule Widget --}}
{{-- Recent Updates Widget --}}
``` ### Notification Bell Component Add to admin layout header: ```blade {{-- In resources/views/components/layouts/admin.blade.php header section --}}
@if($pendingCount = \App\Models\Consultation::pending()->count()) {{ $pendingCount > 99 ? '99+' : $pendingCount }} @endif
``` ### Required Model Scopes Ensure Consultation model has these scopes (from Epic 3): ```php // app/Models/Consultation.php public function scopePending(Builder $query): Builder { return $query->where('status', 'pending'); } public function scopeApproved(Builder $query): Builder { return $query->where('status', 'approved'); } ``` ### Flux UI Components Used - `` - Widget titles - `` - Count badges, status indicators - `` - Action buttons - `` - Empty state messages - `` - Block time slot modal - `` - Form inputs - `` - Reason field - `` - Form field wrapper - `` - Form labels - `` - Bell icon for notifications ## Edge Cases & Error Handling | Scenario | Expected Behavior | |----------|-------------------| | No pending bookings | Show "No pending bookings" message, badge hidden | | Empty today's schedule | Show "No consultations scheduled today" message | | No timeline updates | Show "No recent updates" message | | 100+ pending bookings | Badge shows "99+", list shows 5, "View all" shows count | | Quick action routes don't exist | Buttons still render, navigate to 404 (graceful) | | Mark complete fails | Show error toast, don't update UI | | Block time slot validation fails | Show inline validation errors | | Blocked slot in past | Validation prevents it (after_or_equal:today) | | User deleted after consultation created | Handle with optional chaining on user name | | Polling during action | `wire:poll` pauses during active requests | ## Assumptions 1. **Consultation statuses:** `pending`, `approved`, `completed`, `no-show`, `cancelled`, `blocked` 2. **Blocked slots:** Stored as Consultation records with `status = 'blocked'` and `user_id = null` 3. **Routes exist:** `admin.consultations.index`, `admin.users.create`, `admin.posts.create` from respective epics 4. **Admin middleware:** All routes protected by `auth` and `admin` middleware 5. **Timezone:** All times displayed in application timezone (from config) ## Testing Requirements ### Test File `tests/Feature/Admin/QuickActionsPanelTest.php` ### Test Scenarios ```php use App\Models\User; use App\Models\Consultation; use App\Models\Timeline; use App\Models\TimelineUpdate; use Livewire\Volt\Volt; // Widget Display Tests test('pending bookings widget displays pending count', function () { $admin = User::factory()->admin()->create(); Consultation::factory()->count(3)->pending()->create(); Volt::test('admin.widgets.pending-bookings') ->actingAs($admin) ->assertSee('3') ->assertSee('Pending Bookings'); }); test('pending bookings widget shows empty state when none pending', function () { $admin = User::factory()->admin()->create(); Volt::test('admin.widgets.pending-bookings') ->actingAs($admin) ->assertSee('No pending bookings'); }); test('today schedule widget shows only today approved consultations', function () { $admin = User::factory()->admin()->create(); $client = User::factory()->client()->create(['name' => 'Test Client']); // Today's approved - should show Consultation::factory()->approved()->create([ 'user_id' => $client->id, 'scheduled_date' => today(), 'scheduled_time' => '10:00', ]); // Tomorrow's approved - should NOT show Consultation::factory()->approved()->create([ 'scheduled_date' => today()->addDay(), ]); // Today's pending - should NOT show Consultation::factory()->pending()->create([ 'scheduled_date' => today(), ]); Volt::test('admin.widgets.todays-schedule') ->actingAs($admin) ->assertSee('Test Client') ->assertSee('10:00'); }); test('today schedule widget shows empty state when no consultations', function () { $admin = User::factory()->admin()->create(); Volt::test('admin.widgets.todays-schedule') ->actingAs($admin) ->assertSee('No consultations scheduled today'); }); // Action Tests test('admin can mark consultation as completed', function () { $admin = User::factory()->admin()->create(); $consultation = Consultation::factory()->approved()->create([ 'scheduled_date' => today(), ]); Volt::test('admin.widgets.todays-schedule') ->actingAs($admin) ->call('markComplete', $consultation->id) ->assertHasNoErrors(); expect($consultation->fresh()->status)->toBe('completed'); }); test('admin can mark consultation as no-show', function () { $admin = User::factory()->admin()->create(); $consultation = Consultation::factory()->approved()->create([ 'scheduled_date' => today(), ]); Volt::test('admin.widgets.todays-schedule') ->actingAs($admin) ->call('markNoShow', $consultation->id) ->assertHasNoErrors(); expect($consultation->fresh()->status)->toBe('no-show'); }); // Block Time Slot Tests test('admin can block a time slot', function () { $admin = User::factory()->admin()->create(); Volt::test('admin.widgets.quick-actions') ->actingAs($admin) ->call('openBlockModal') ->set('blockDate', today()->format('Y-m-d')) ->set('blockStartTime', '09:00') ->set('blockEndTime', '10:00') ->set('blockReason', 'Personal appointment') ->call('blockTimeSlot') ->assertHasNoErrors(); expect(Consultation::where('status', 'blocked')->count())->toBe(1); }); test('block time slot validates required fields', function () { $admin = User::factory()->admin()->create(); Volt::test('admin.widgets.quick-actions') ->actingAs($admin) ->call('openBlockModal') ->set('blockDate', '') ->call('blockTimeSlot') ->assertHasErrors(['blockDate']); }); test('block time slot prevents past dates', function () { $admin = User::factory()->admin()->create(); Volt::test('admin.widgets.quick-actions') ->actingAs($admin) ->call('openBlockModal') ->set('blockDate', today()->subDay()->format('Y-m-d')) ->set('blockStartTime', '09:00') ->set('blockEndTime', '10:00') ->call('blockTimeSlot') ->assertHasErrors(['blockDate']); }); // Recent Updates Widget Tests test('recent updates widget shows last 5 updates', function () { $admin = User::factory()->admin()->create(); $timeline = Timeline::factory()->create(); TimelineUpdate::factory()->count(7)->create(['timeline_id' => $timeline->id]); Volt::test('admin.widgets.recent-updates') ->actingAs($admin) ->assertViewHas('recentUpdates', fn($updates) => $updates->count() === 5); }); // Access Control Tests test('non-admin cannot access dashboard widgets', function () { $client = User::factory()->client()->create(); $this->actingAs($client) ->get(route('admin.dashboard')) ->assertForbidden(); }); // Notification Bell Tests test('notification bell shows pending count in header', function () { $admin = User::factory()->admin()->create(); Consultation::factory()->count(5)->pending()->create(); $this->actingAs($admin) ->get(route('admin.dashboard')) ->assertSee('5'); // Badge count }); test('notification bell hidden when no pending items', function () { $admin = User::factory()->admin()->create(); // No badge should render when count is 0 $this->actingAs($admin) ->get(route('admin.dashboard')) ->assertSuccessful(); }); ``` ### Manual Testing Checklist - [ ] Verify polling updates data every 30 seconds without page reload - [ ] Verify responsive layout on mobile (375px) - widgets stack vertically - [ ] Verify responsive layout on tablet (768px) - 2 column grid - [ ] Verify responsive layout on desktop (1200px+) - 3 column grid - [ ] Verify pending badge uses red/warning color when > 0 - [ ] Verify quick action buttons navigate to correct pages - [ ] Verify block time slot modal opens and closes correctly - [ ] Verify RTL layout works correctly (Arabic) - [ ] Verify notification bell shows in header on all admin pages - [ ] Verify "Complete" and "No-show" buttons work without page refresh ## Definition of Done - [ ] All four widgets display correctly with accurate data - [ ] Widgets auto-refresh via Livewire polling (30s interval) - [ ] Quick action buttons navigate to correct routes - [ ] Block time slot modal creates blocked consultation record - [ ] Mark complete/no-show actions update consultation status - [ ] Notification bell shows pending count in admin header - [ ] Empty states render gracefully for all widgets - [ ] Edge cases handled (100+ items, missing data) - [ ] All tests pass - [ ] Responsive layout works on mobile, tablet, desktop - [ ] RTL support for Arabic - [ ] Code formatted with Pint ## Out of Scope - WebSocket real-time updates (using polling instead) - Push notifications to browser - Email notifications for pending items - Widget position customization - Dashboard layout preferences ## Estimation **Complexity:** Medium | **Effort:** 4-5 hours