load(['questionnaire.questionnaireSet.questions']); // Attendance by session $bySession = $program->attendances() ->selectRaw('attendance_session, COUNT(*) as total') ->groupBy('attendance_session') ->pluck('total', 'attendance_session') ->toArray(); // Attendance by source $bySource = $program->attendances() ->selectRaw('attendance_source, COUNT(*) as total') ->groupBy('attendance_source') ->pluck('total', 'attendance_source') ->toArray(); // Certificate status breakdown $certStats = $program->certificates() ->selectRaw('status, COUNT(*) as total') ->groupBy('status') ->pluck('total', 'status') ->toArray(); // Response rate + question stats $pq = $program->questionnaire; $responseRate = null; $questionStats = []; $totalResponses = 0; if ($pq && $pq->is_confirmed) { $totalAttended = array_sum($bySession); // reuse already-fetched data $totalResponses = QuestionnaireResponse::where('program_id', $program->id)->count(); $responseRate = $totalAttended > 0 ? round($totalResponses / $totalAttended * 100) : 0; $questions = $pq->questionnaireSet->questions ?? collect(); // Load ALL answers in one query, group by question — avoids N+1 $allAnswers = QuestionnaireAnswer::whereIn('questionnaire_question_id', $questions->pluck('id')) ->get() ->groupBy('questionnaire_question_id'); foreach ($questions as $q) { $answers = $allAnswers->get($q->id, collect()); if ($q->question_type === 'rating') { $values = $answers->map(fn($a) => is_array($a->answer_value) ? (int) ($a->answer_value[0] ?? 0) : (int) $a->answer_value); $questionStats[] = [ 'id' => $q->id, 'text' => $q->question_text, 'type' => 'rating', 'average' => $values->count() > 0 ? round($values->avg(), 2) : null, 'count' => $values->count(), ]; } elseif (in_array($q->question_type, ['single_choice', 'multiple_choice'])) { $counts = []; foreach ($answers as $row) { $items = is_array($row->answer_value) ? $row->answer_value : [$row->answer_value]; foreach ($items as $item) { $counts[$item] = ($counts[$item] ?? 0) + 1; } } $questionStats[] = [ 'id' => $q->id, 'text' => $q->question_text, 'type' => $q->question_type, 'options' => $q->options_json ?? [], 'counts' => $counts, 'total' => $answers->count(), ]; } } } // Reuse data already computed above — no extra queries $summary = [ 'total_attendances' => array_sum($bySession), 'pre_registered' => $bySource['pre_registered_staff'] ?? 0, 'walk_in' => $bySource['walk_in_external'] ?? 0, 'total_certificates' => array_sum($certStats), 'generated_certs' => ($certStats['generated'] ?? 0) + ($certStats['emailed'] ?? 0) + ($certStats['downloaded'] ?? 0), 'downloaded_certs' => $certStats['downloaded'] ?? 0, 'total_responses' => $totalResponses, ]; return view('admin.programs.statistics.show', compact( 'program', 'summary', 'bySession', 'bySource', 'certStats', 'responseRate', 'questionStats' )); } public function export(Program $program): Response { $rows = $program->attendances() ->with('participant') ->get() ->map(fn($a) => [ $a->participant->name, $a->participant->agency ?: '', $a->attendance_session, $a->attendance_source, $a->checked_in_at->format('d/m/Y H:i'), ]); $csv = "\xEF\xBB\xBF"; $csv .= implode(',', ['Nama', 'Agensi', 'Sesi', 'Sumber', 'Masa Check-In']) . "\n"; foreach ($rows as $row) { $csv .= implode(',', array_map(fn($v) => '"' . str_replace('"', '""', $v) . '"', $row)) . "\n"; } $filename = 'statistik-' . str($program->title)->slug() . '-' . now()->format('Ymd') . '.csv'; return response($csv, 200, [ 'Content-Type' => 'text/csv; charset=UTF-8', 'Content-Disposition' => 'attachment; filename="' . $filename . '"', ]); } }