|
| 1 | +<?php |
| 2 | + |
| 3 | +namespace App\Livewire; |
| 4 | + |
| 5 | +use App\Models\UserChecklist; |
| 6 | +use App\Models\Person; |
| 7 | +use App\Models\Family; |
| 8 | +use Illuminate\Contracts\View\View; |
| 9 | +use Livewire\Component; |
| 10 | +use Illuminate\Support\Collection; |
| 11 | +use Illuminate\Support\Carbon; |
| 12 | + |
| 13 | +class ResearchProgressWidget extends Component |
| 14 | +{ |
| 15 | + public int $selectedPeriod = 30; // days |
| 16 | + public string $selectedSubjectType = 'all'; |
| 17 | + public bool $showDetails = false; |
| 18 | + |
| 19 | + /** @var array<string, mixed> */ |
| 20 | + public array $stats = [ |
| 21 | + 'total_checklists' => 0, |
| 22 | + 'completed_checklists' => 0, |
| 23 | + 'in_progress_checklists' => 0, |
| 24 | + 'completion_rate' => 0, |
| 25 | + 'overall_progress' => 0, |
| 26 | + ]; |
| 27 | + |
| 28 | + /** @var array<string, mixed> */ |
| 29 | + public array $recentActivity = [ |
| 30 | + 'items' => [], |
| 31 | + ]; |
| 32 | + |
| 33 | + /** @var array<string, Collection> */ |
| 34 | + public array $upcomingDeadlines = [ |
| 35 | + 'overdue' => null, |
| 36 | + 'upcoming' => null, |
| 37 | + ]; |
| 38 | + |
| 39 | + /** @var array<string, mixed> */ |
| 40 | + public array $subjectProgress = [ |
| 41 | + 'person_progress' => [ |
| 42 | + 'completed' => 0, |
| 43 | + 'total' => 0, |
| 44 | + 'progress_percentage' => 0, |
| 45 | + ], |
| 46 | + 'family_progress' => [ |
| 47 | + 'completed' => 0, |
| 48 | + 'total' => 0, |
| 49 | + 'progress_percentage' => 0, |
| 50 | + ], |
| 51 | + 'top_persons' => null, |
| 52 | + 'top_families' => null, |
| 53 | + ]; |
| 54 | + |
| 55 | + public function mount(): void |
| 56 | + { |
| 57 | + // Initialize collections to avoid null method calls in the Blade |
| 58 | + $this->upcomingDeadlines['overdue'] = collect(); |
| 59 | + $this->upcomingDeadlines['upcoming'] = collect(); |
| 60 | + $this->subjectProgress['top_persons'] = collect(); |
| 61 | + $this->subjectProgress['top_families'] = collect(); |
| 62 | + |
| 63 | + $this->refreshData(); |
| 64 | + } |
| 65 | + |
| 66 | + public function updatedSelectedPeriod(): void |
| 67 | + { |
| 68 | + $this->refreshData(); |
| 69 | + } |
| 70 | + |
| 71 | + public function updatedSelectedSubjectType(): void |
| 72 | + { |
| 73 | + $this->refreshData(); |
| 74 | + } |
| 75 | + |
| 76 | + public function toggleDetails(): void |
| 77 | + { |
| 78 | + $this->showDetails = ! $this->showDetails; |
| 79 | + } |
| 80 | + |
| 81 | + public function render(): View |
| 82 | + { |
| 83 | + return view('livewire.research-progress-widget'); |
| 84 | + } |
| 85 | + |
| 86 | + protected function refreshData(): void |
| 87 | + { |
| 88 | + $userId = auth()->id(); |
| 89 | + if (! $userId) { |
| 90 | + return; |
| 91 | + } |
| 92 | + |
| 93 | + $periodStart = Carbon::now()->subDays($this->selectedPeriod); |
| 94 | + |
| 95 | + // Base query for user's checklists |
| 96 | + $base = UserChecklist::query()->where('user_id', $userId); |
| 97 | + if ($this->selectedSubjectType !== 'all') { |
| 98 | + $base->where('subject_type', $this->selectedSubjectType); |
| 99 | + } |
| 100 | + |
| 101 | + $total = (clone $base)->count(); |
| 102 | + $completed = (clone $base)->completed()->count(); |
| 103 | + $inProgress = (clone $base)->active()->count(); |
| 104 | + |
| 105 | + $completionRate = $total > 0 ? round(($completed / $total) * 100, 2) : 0.0; |
| 106 | + // Overall progress approximated by completion rate when item-level data not available |
| 107 | + $overallProgress = $completionRate; |
| 108 | + |
| 109 | + $this->stats = [ |
| 110 | + 'total_checklists' => $total, |
| 111 | + 'completed_checklists' => $completed, |
| 112 | + 'in_progress_checklists' => $inProgress, |
| 113 | + 'completion_rate' => $completionRate, |
| 114 | + 'overall_progress' => $overallProgress, |
| 115 | + ]; |
| 116 | + |
| 117 | + // Recent activity: latest completed checklists in period, adapted to Blade structure |
| 118 | + $recentCompleted = (clone $base) |
| 119 | + ->whereNotNull('completed_at') |
| 120 | + ->where('completed_at', '>=', $periodStart) |
| 121 | + ->orderByDesc('completed_at') |
| 122 | + ->limit(20) |
| 123 | + ->get(['id','name','completed_at','subject_type','subject_id']); |
| 124 | + |
| 125 | + // Map to objects with userChecklist relation-like structure expected by the Blade |
| 126 | + $this->recentActivity = [ |
| 127 | + 'items' => $recentCompleted->map(function (UserChecklist $c) { |
| 128 | + return (object) [ |
| 129 | + 'userChecklist' => $c, |
| 130 | + 'completed_at' => $c->completed_at, |
| 131 | + ]; |
| 132 | + }), |
| 133 | + ]; |
| 134 | + |
| 135 | + // Upcoming deadlines |
| 136 | + $this->upcomingDeadlines['overdue'] = (clone $base)->overdue()->orderBy('due_date')->limit(20)->get(); |
| 137 | + $this->upcomingDeadlines['upcoming'] = (clone $base) |
| 138 | + ->whereNotNull('due_date') |
| 139 | + ->where('due_date', '>=', Carbon::today()) |
| 140 | + ->where('status', '!=', UserChecklist::STATUS_COMPLETED) |
| 141 | + ->orderBy('due_date') |
| 142 | + ->limit(20) |
| 143 | + ->get(); |
| 144 | + |
| 145 | + // Subject progress |
| 146 | + $personTotal = (clone $base)->where('subject_type', Person::class)->count(); |
| 147 | + $personCompleted = (clone $base)->where('subject_type', Person::class)->completed()->count(); |
| 148 | + $familyTotal = (clone $base)->where('subject_type', Family::class)->count(); |
| 149 | + $familyCompleted = (clone $base)->where('subject_type', Family::class)->completed()->count(); |
| 150 | + |
| 151 | + $this->subjectProgress['person_progress'] = [ |
| 152 | + 'completed' => $personCompleted, |
| 153 | + 'total' => $personTotal, |
| 154 | + 'progress_percentage' => $personTotal > 0 ? round(($personCompleted / $personTotal) * 100, 2) : 0, |
| 155 | + ]; |
| 156 | + $this->subjectProgress['family_progress'] = [ |
| 157 | + 'completed' => $familyCompleted, |
| 158 | + 'total' => $familyTotal, |
| 159 | + 'progress_percentage' => $familyTotal > 0 ? round(($familyCompleted / $familyTotal) * 100, 2) : 0, |
| 160 | + ]; |
| 161 | + |
| 162 | + // Top subjects by checklist count (completed first, then total) |
| 163 | + $topPersons = (clone $base) |
| 164 | + ->where('subject_type', Person::class) |
| 165 | + ->selectRaw('subject_id, sum(case when status = ? then 1 else 0 end) as completed_count, count(*) as total_count', [UserChecklist::STATUS_COMPLETED]) |
| 166 | + ->groupBy('subject_id') |
| 167 | + ->orderByDesc('completed_count') |
| 168 | + ->orderByDesc('total_count') |
| 169 | + ->limit(5) |
| 170 | + ->get() |
| 171 | + ->map(function ($row) { |
| 172 | + $person = Person::find($row->subject_id); |
| 173 | + if ($person) { |
| 174 | + $person->progress_percentage = $row->total_count > 0 ? round(($row->completed_count / $row->total_count) * 100, 0) : 0; |
| 175 | + } |
| 176 | + return $person; |
| 177 | + }) |
| 178 | + ->filter(); |
| 179 | + |
| 180 | + $topFamilies = (clone $base) |
| 181 | + ->where('subject_type', Family::class) |
| 182 | + ->selectRaw('subject_id, sum(case when status = ? then 1 else 0 end) as completed_count, count(*) as total_count', [UserChecklist::STATUS_COMPLETED]) |
| 183 | + ->groupBy('subject_id') |
| 184 | + ->orderByDesc('completed_count') |
| 185 | + ->orderByDesc('total_count') |
| 186 | + ->limit(5) |
| 187 | + ->get() |
| 188 | + ->map(function ($row) { |
| 189 | + $family = Family::find($row->subject_id); |
| 190 | + if ($family) { |
| 191 | + $family->progress_percentage = $row->total_count > 0 ? round(($row->completed_count / $row->total_count) * 100, 0) : 0; |
| 192 | + } |
| 193 | + return $family; |
| 194 | + }) |
| 195 | + ->filter(); |
| 196 | + |
| 197 | + $this->subjectProgress['top_persons'] = $topPersons; |
| 198 | + $this->subjectProgress['top_families'] = $topFamilies; |
| 199 | + } |
| 200 | +} |
0 commit comments