# Story 6.2: Analytics Charts ## Epic Reference **Epic 6:** Admin Dashboard ## User Story As an **admin**, I want **visual charts showing trends and historical data**, So that **I can identify patterns in client acquisition and consultation outcomes to make informed business decisions**. ## 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 structure and route | | User Model | Epic 2 | `users` table with `created_at` for tracking new clients | | Consultation Model | Epic 3 | `consultations` table with `consultation_type`, `status`, `scheduled_date` | | Admin Layout | Epic 1 | Admin authenticated layout with navigation | **References:** - Epic 6 details: `docs/epics/epic-6-admin-dashboard.md` - PRD Analytics Section: `docs/prd.md` Section 11.1 (Analytics & Reporting) - PRD Dashboard Section: `docs/prd.md` Section 5.7 (Admin Dashboard) - Story 6.1 for dashboard structure: `docs/stories/story-6.1-dashboard-overview-statistics.md` ## Acceptance Criteria ### Monthly Trends Chart (Line Chart) - [ ] Display new clients per month (from `users.created_at`) - [ ] Display consultations per month (from `consultations.scheduled_date`) - [ ] Two data series on same chart with legend - [ ] X-axis: Month labels (e.g., "Jan 2025", "Feb 2025") - [ ] Y-axis: Count values with appropriate scale ### Consultation Breakdown Chart (Pie/Donut) - [ ] Show free vs paid consultation ratio - [ ] Display percentage labels on segments - [ ] Legend showing "Free" and "Paid" with counts ### No-show Rate Chart (Line Chart) - [ ] Monthly no-show percentage trend - [ ] X-axis: Month labels - [ ] Y-axis: Percentage (0-100%) - [ ] Visual threshold line at concerning rate (e.g., 20%) ### Date Range Selector - [ ] Preset options: Last 6 months, Last 12 months - [ ] Custom date range picker (start/end month) - [ ] Charts update when range changes - [ ] Default selection: Last 6 months ### Chart Features - [ ] Tooltips showing exact values on hover - [ ] Responsive sizing (charts resize with viewport) - [ ] Bilingual labels (Arabic/English based on locale) - [ ] Loading state while fetching data - [ ] Empty state when no data available ### Design - [ ] Charts section below statistics cards from Story 6.1 - [ ] Card-based layout for each chart - [ ] Navy blue and gold color scheme per PRD Section 7.1 - [ ] Responsive grid (1 column mobile, 2 columns tablet+) ## Technical Implementation ### Files to Create/Modify | File | Purpose | |------|---------| | `resources/views/livewire/admin/dashboard.blade.php` | Add charts section to existing dashboard | | `app/Services/AnalyticsService.php` | Data aggregation service for chart data | ### Integration with Story 6.1 Dashboard The charts will be added as a new section in the existing dashboard component from Story 6.1. Add below the statistics cards section. ### Component Updates (Volt Class-Based) Add to the existing dashboard component: ```php $this->getChartData(), ]; } public function updatedChartPeriod(): void { // Livewire will re-render with new data } public function setCustomRange(string $start, string $end): void { $this->customStart = $start; $this->customEnd = $end; $this->chartPeriod = 'custom'; } private function getChartData(): array { $service = app(AnalyticsService::class); $months = match($this->chartPeriod) { '6m' => 6, '12m' => 12, 'custom' => $this->getCustomMonthCount(), default => 6, }; $startDate = $this->chartPeriod === 'custom' && $this->customStart ? Carbon::parse($this->customStart)->startOfMonth() : now()->subMonths($months - 1)->startOfMonth(); return [ 'labels' => $service->getMonthLabels($startDate, $months), 'newClients' => $service->getMonthlyNewClients($startDate, $months), 'consultations' => $service->getMonthlyConsultations($startDate, $months), 'consultationBreakdown' => $service->getConsultationTypeBreakdown($startDate, $months), 'noShowRates' => $service->getMonthlyNoShowRates($startDate, $months), ]; } private function getCustomMonthCount(): int { if (!$this->customStart || !$this->customEnd) { return 6; } return Carbon::parse($this->customStart) ->diffInMonths(Carbon::parse($this->customEnd)) + 1; } }; ?> ``` ### Analytics Service Create `app/Services/AnalyticsService.php`: ```php map(fn($i) => $startDate->copy()->addMonths($i)->translatedFormat('M Y')) ->toArray(); } public function getMonthlyNewClients(Carbon $startDate, int $months): array { $endDate = $startDate->copy()->addMonths($months); $data = User::query() ->whereIn('user_type', ['individual', 'company']) ->whereBetween('created_at', [$startDate, $endDate]) ->selectRaw("DATE_FORMAT(created_at, '%Y-%m') as month, COUNT(*) as count") ->groupBy('month') ->pluck('count', 'month'); return $this->fillMonthlyData($startDate, $months, $data); } public function getMonthlyConsultations(Carbon $startDate, int $months): array { $endDate = $startDate->copy()->addMonths($months); $data = Consultation::query() ->whereBetween('scheduled_date', [$startDate, $endDate]) ->selectRaw("DATE_FORMAT(scheduled_date, '%Y-%m') as month, COUNT(*) as count") ->groupBy('month') ->pluck('count', 'month'); return $this->fillMonthlyData($startDate, $months, $data); } public function getConsultationTypeBreakdown(Carbon $startDate, int $months): array { $endDate = $startDate->copy()->addMonths($months); return [ 'free' => Consultation::whereBetween('scheduled_date', [$startDate, $endDate]) ->where('consultation_type', 'free')->count(), 'paid' => Consultation::whereBetween('scheduled_date', [$startDate, $endDate]) ->where('consultation_type', 'paid')->count(), ]; } public function getMonthlyNoShowRates(Carbon $startDate, int $months): array { $endDate = $startDate->copy()->addMonths($months); $results = []; for ($i = 0; $i < $months; $i++) { $monthStart = $startDate->copy()->addMonths($i); $monthEnd = $monthStart->copy()->endOfMonth(); $total = Consultation::whereBetween('scheduled_date', [$monthStart, $monthEnd]) ->whereIn('status', ['completed', 'no-show']) ->count(); $noShows = Consultation::whereBetween('scheduled_date', [$monthStart, $monthEnd]) ->where('status', 'no-show') ->count(); $results[] = $total > 0 ? round(($noShows / $total) * 100, 1) : 0; } return $results; } private function fillMonthlyData(Carbon $startDate, int $months, Collection $data): array { return collect(range(0, $months - 1)) ->map(fn($i) => $data->get($startDate->copy()->addMonths($i)->format('Y-m'), 0)) ->toArray(); } } ``` ### Chart.js Integration with Livewire Use `wire:ignore` to prevent Livewire from re-rendering chart canvas, and Alpine.js to manage Chart.js instances: ```blade {{-- Charts Section --}}