>} */ public function buildForSurvey(SurveyModel $survey): array { $overview = $this->surveySubmissionService->getSubmissionOverview($survey); $finishedSubmissions = array_values(array_filter( $overview, static fn (array $submission): bool => '1' === (string) ($submission['isFinished'] ?? '') )); $completedSubmissionCount = count($finishedSubmissions); $questions = $this->surveyQuestionRepository->findAllBySurvey((int) $survey->id); $answers = $this->surveyAnswerRepository->findFinishedAnswersBySurvey((int) $survey->id); return [ 'completedSubmissionCount' => $completedSubmissionCount, 'questionResults' => $this->buildQuestionResults($questions, $answers, $completedSubmissionCount), ]; } /** * @param list $questions * @param list $answers * * @return list> */ private function buildQuestionResults(array $questions, array $answers, int $completedSubmissionCount): array { $results = []; foreach ($questions as $question) { $key = $this->buildQuestionResultKey((int) $question->id, (string) $question->type); $results[$key] = $this->buildResultSkeleton( (int) $question->id, (string) $question->type, (string) $question->question, (string) ($question->description ?? ''), (int) ($question->sorting ?? 0), ); $results[$key]['_choiceOptions'] = $this->getChoiceOptions($question); $results[$key]['_allowMaybe'] = '1' === (string) $question->allowMaybe; } foreach ($answers as $answer) { $questionId = (int) ($answer['questionId'] ?? 0); $questionType = (string) ($answer['questionType'] ?? 'text'); $questionLabel = trim((string) ($answer['questionLabel'] ?? '')) ?: 'Frage #'.$questionId; $questionDescription = (string) ($answer['questionDescription'] ?? ''); $key = $this->buildQuestionResultKey($questionId, $questionType); if (!isset($results[$key])) { $results[$key] = $this->buildResultSkeleton( $questionId, $questionType, $questionLabel, $questionDescription, (int) ($answer['questionSorting'] ?? 0), ); } $results[$key]['_values'][] = (string) ($answer['value'] ?? ''); } foreach ($results as $key => $result) { $results[$key] = $this->finalizeQuestionResult($result, $completedSubmissionCount); } uasort($results, static function (array $left, array $right): int { $sortingComparison = ((int) ($left['_sorting'] ?? 0)) <=> ((int) ($right['_sorting'] ?? 0)); if (0 !== $sortingComparison) { return $sortingComparison; } return strcasecmp((string) ($left['question'] ?? ''), (string) ($right['question'] ?? '')); }); return array_values($results); } private function buildQuestionResultKey(int $questionId, string $questionType): string { // Bewusst nur Frage-ID + Typ: So werden Antwort-Snapshots und die aktuelle // Frage auch dann zusammengeführt, wenn das Label nachträglich geändert wurde. return implode('|', [$questionId, $questionType]); } /** * @return array */ private function buildResultSkeleton(int $questionId, string $questionType, string $questionLabel, string $questionDescription, int $sorting): array { return [ 'id' => $questionId, 'question' => $questionLabel, 'description' => $questionDescription, 'type' => $questionType, 'totalAnswers' => 0, 'responseRate' => 0, 'chart' => null, '_sorting' => $sorting, '_values' => [], '_choiceOptions' => [], '_allowMaybe' => false, ]; } /** * @param array $result * * @return array */ private function finalizeQuestionResult(array $result, int $completedSubmissionCount): array { $values = $result['_values'] ?? []; $values = \is_array($values) ? array_values(array_map('strval', $values)) : []; $totalAnswers = count($values); $result['totalAnswers'] = $totalAnswers; $result['responseRate'] = $completedSubmissionCount > 0 ? (int) round(($totalAnswers / $completedSubmissionCount) * 100) : 0; switch ((string) ($result['type'] ?? 'text')) { case 'yes_no_maybe': $optionCounts = ['Ja' => 0, 'Nein' => 0]; // Wie beim Excel-Export: Bei erlaubtem "Vielleicht" die Zeile auch dann // zeigen, wenn (noch) niemand "Vielleicht" gewählt hat. if (!empty($result['_allowMaybe'])) { $optionCounts['Vielleicht'] = 0; } foreach ($values as $value) { $normalizedValue = mb_strtolower(trim($value)); if ('yes' === $normalizedValue) { ++$optionCounts['Ja']; } elseif ('no' === $normalizedValue) { ++$optionCounts['Nein']; } elseif ('maybe' === $normalizedValue) { $optionCounts['Vielleicht'] = ($optionCounts['Vielleicht'] ?? 0) + 1; } } $result['options'] = $this->buildOptionStats($optionCounts); $result['chart'] = $this->buildPieChartConfig($result['options']); break; case 'choice': $optionCounts = []; foreach (($result['_choiceOptions'] ?? []) as $choiceOption) { $optionCounts[(string) $choiceOption] = 0; } foreach ($values as $value) { foreach (array_filter(array_map('trim', explode(' | ', $value)), static fn (string $entry): bool => '' !== $entry) as $selectedOption) { $optionCounts[$selectedOption] = ($optionCounts[$selectedOption] ?? 0) + 1; } } $result['options'] = $this->buildOptionStats($optionCounts); $result['chart'] = $this->buildPieChartConfig($result['options']); break; case 'range': $numericValues = array_values(array_map(static fn (string $value): int => (int) $value, $values)); $distribution = []; foreach ($numericValues as $numericValue) { $label = (string) $numericValue; $distribution[$label] = ($distribution[$label] ?? 0) + 1; } ksort($distribution, SORT_NATURAL); $result['minimum'] = [] !== $numericValues ? min($numericValues) : null; $result['maximum'] = [] !== $numericValues ? max($numericValues) : null; $result['average'] = [] !== $numericValues ? round(array_sum($numericValues) / count($numericValues), 2) : null; $result['distribution'] = $this->buildOptionStats($distribution); $result['chart'] = $this->buildRangeChartConfig($result['distribution']); break; case 'text': default: $result['responses'] = array_values(array_filter($values, static fn (string $value): bool => '' !== trim($value))); $result['responseSummary'] = $this->buildTextResponseSummary($result['responses']); $result['chart'] = null; break; } unset($result['_sorting'], $result['_values'], $result['_choiceOptions'], $result['_allowMaybe']); return $result; } /** * @return list */ private function getChoiceOptions(SurveyContentModel $question): array { $options = []; for ($index = 1; $index <= 10; ++$index) { $option = trim((string) $question->{'answerOption'.$index}); if ('' !== $option) { $options[] = $option; } } return $options; } /** * @param array $optionCounts * * @return list */ private function buildOptionStats(array $optionCounts): array { $total = array_sum($optionCounts); $options = []; foreach ($optionCounts as $label => $count) { $options[] = [ 'label' => $label, 'count' => $count, 'percentage' => $total > 0 ? (int) round(($count / $total) * 100) : 0, ]; } return $options; } /** * @param list $options * * @return array|null */ private function buildPieChartConfig(array $options): ?array { if ([] === $options) { return null; } $palette = $this->buildChartPalette(count($options)); return [ 'type' => 'pie', 'data' => [ 'labels' => array_column($options, 'label'), 'datasets' => [[ 'data' => array_column($options, 'count'), 'backgroundColor' => array_column($palette, 'background'), 'borderColor' => array_column($palette, 'border'), 'borderWidth' => 2, 'hoverOffset' => 18, ]], ], 'options' => [ 'responsive' => true, 'maintainAspectRatio' => false, 'plugins' => [ 'legend' => [ 'position' => 'bottom', 'labels' => [ 'usePointStyle' => true, 'boxWidth' => 12, 'padding' => 18, 'color' => '#555E68', 'font' => [ 'family' => 'PT Sans Narrow', 'size' => 12, 'weight' => '600', ], ], ], 'tooltip' => [ 'backgroundColor' => 'rgba(34, 34, 34, 0.92)', 'titleFont' => [ 'family' => 'Blogger Sans', 'size' => 14, ], 'bodyFont' => [ 'family' => 'PT Sans Narrow', 'size' => 12, ], ], ], ], ]; } /** * @param list $distribution * * @return array|null */ private function buildRangeChartConfig(array $distribution): ?array { if ([] === $distribution) { return null; } $pointStyles = ['circle', 'rectRounded', 'triangle', 'star', 'rectRot', 'crossRot']; $palette = $this->buildChartPalette(count($distribution)); return [ 'type' => 'line', 'data' => [ 'labels' => array_column($distribution, 'label'), 'datasets' => [[ 'label' => 'Antworten', 'data' => array_column($distribution, 'count'), 'borderColor' => '#0064AD', 'backgroundColor' => 'rgba(236, 124, 50, 0.12)', 'pointBackgroundColor' => array_column($palette, 'border'), 'pointBorderColor' => '#ffffff', 'pointBorderWidth' => 3, 'pointHoverBorderWidth' => 3, 'pointRadius' => 7, 'pointHoverRadius' => 9, 'pointStyle' => array_map( static fn (int $index): string => $pointStyles[$index % count($pointStyles)], array_keys($distribution) ), 'fill' => true, 'tension' => 0.32, ]], ], 'options' => [ 'responsive' => true, 'maintainAspectRatio' => false, 'plugins' => [ 'legend' => [ 'display' => false, ], 'tooltip' => [ 'backgroundColor' => 'rgba(34, 34, 34, 0.92)', 'titleFont' => [ 'family' => 'Blogger Sans', 'size' => 14, ], 'bodyFont' => [ 'family' => 'PT Sans Narrow', 'size' => 12, ], ], ], 'scales' => [ 'x' => [ 'grid' => [ 'display' => false, ], 'ticks' => [ 'color' => '#555E68', 'font' => [ 'family' => 'PT Sans Narrow', 'weight' => '600', ], ], ], 'y' => [ 'beginAtZero' => true, 'ticks' => [ 'precision' => 0, 'color' => '#555E68', 'font' => [ 'family' => 'PT Sans Narrow', 'weight' => '600', ], ], 'grid' => [ 'color' => 'rgba(162, 168, 180, 0.22)', ], ], ], ], ]; } /** * @param list $responses * * @return list */ private function buildTextResponseSummary(array $responses): array { if ([] === $responses) { return []; } $counts = []; foreach ($responses as $response) { $label = trim($response); if ('' === $label) { continue; } $counts[$label] = ($counts[$label] ?? 0) + 1; } arsort($counts); $summary = []; $remainingCount = 0; $index = 0; foreach ($counts as $label => $count) { if ($index < 5) { $summary[$label] = $count; } else { $remainingCount += $count; } ++$index; } if ($remainingCount > 0) { $summary['Weitere Antworten'] = $remainingCount; } return $this->buildOptionStats($summary); } /** * @return list */ private function buildChartPalette(int $count): array { $palette = [ ['background' => 'rgba(0, 100, 173, 0.16)', 'border' => '#0064AD'], ['background' => 'rgba(236, 124, 50, 0.24)', 'border' => '#EC7C32'], ['background' => 'rgba(157, 82, 118, 0.18)', 'border' => '#9D5276'], ['background' => 'rgba(0, 174, 151, 0.22)', 'border' => '#00AE97'], ['background' => 'rgba(252, 228, 214, 0.9)', 'border' => '#EC7C32'], ['background' => 'rgba(162, 168, 180, 0.28)', 'border' => '#555E68'], ['background' => 'rgba(233, 233, 235, 0.9)', 'border' => '#A2A8B4'], ['background' => 'rgba(34, 34, 34, 0.12)', 'border' => '#222222'], ]; $colors = []; for ($index = 0; $index < max(1, $count); ++$index) { $colors[] = $palette[$index % count($palette)]; } return $colors; } }