Initial survey bundle import

This commit is contained in:
Jürgen Mummert
2026-05-17 17:31:28 +02:00
commit b8475e3a57
84 changed files with 7157 additions and 0 deletions
+395
View File
@@ -0,0 +1,395 @@
<?php
declare(strict_types=1);
namespace Mummert\SurveyBundle\Service;
use Mummert\SurveyBundle\Model\SurveyContentModel;
use Mummert\SurveyBundle\Model\SurveyModel;
use Mummert\SurveyBundle\Repository\SurveyAnswerRepository;
use Mummert\SurveyBundle\Repository\SurveyQuestionRepository;
final class SurveyResultsViewService
{
public function __construct(
private readonly SurveyQuestionRepository $surveyQuestionRepository,
private readonly SurveyAnswerRepository $surveyAnswerRepository,
private readonly SurveySubmissionService $surveySubmissionService,
) {
}
/**
* @return array{completedSubmissionCount:int,questionResults:list<array<string, mixed>>}
*/
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<SurveyContentModel> $questions
* @param list<array{questionId:int,value:string}> $answers
*
* @return list<array<string, mixed>>
*/
private function buildQuestionResults(array $questions, array $answers, int $completedSubmissionCount): array
{
$answersByQuestion = [];
foreach ($answers as $answer) {
$answersByQuestion[$answer['questionId']][] = $answer['value'];
}
$results = [];
foreach ($questions as $question) {
$questionId = (int) $question->id;
$values = $answersByQuestion[$questionId] ?? [];
$totalAnswers = count($values);
$responseRate = $completedSubmissionCount > 0 ? (int) round(($totalAnswers / $completedSubmissionCount) * 100) : 0;
$result = [
'id' => $questionId,
'question' => (string) $question->question,
'description' => (string) ($question->description ?? ''),
'type' => (string) $question->type,
'totalAnswers' => $totalAnswers,
'responseRate' => $responseRate,
'chart' => null,
];
switch ((string) $question->type) {
case 'yes_no_maybe':
$optionCounts = [
'Ja' => 0,
'Nein' => 0,
];
if ('1' === (string) $question->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 && isset($optionCounts['Vielleicht'])) {
++$optionCounts['Vielleicht'];
}
}
$result['options'] = $this->buildOptionStats($optionCounts);
$result['chart'] = $this->buildPieChartConfig($result['options']);
break;
case 'choice':
$optionCounts = [];
for ($index = 1; $index <= 10; ++$index) {
$option = trim((string) $question->{'answerOption'.$index});
if ('' !== $option) {
$optionCounts[$option] = 0;
}
}
foreach ($values as $value) {
foreach (array_filter(array_map('trim', explode(' | ', $value)), static fn (string $entry): bool => '' !== $entry) as $selectedOption) {
if (!array_key_exists($selectedOption, $optionCounts)) {
$optionCounts[$selectedOption] = 0;
}
++$optionCounts[$selectedOption];
}
}
$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'] = $this->buildPieChartConfig($result['responseSummary']);
break;
}
$results[] = $result;
}
return $results;
}
/**
* @param array<string, int> $optionCounts
*
* @return list<array{label:string,count:int,percentage:int}>
*/
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<array{label:string,count:int,percentage:int}> $options
*
* @return array<string, mixed>|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<array{label:string,count:int,percentage:int}> $distribution
*
* @return array<string, mixed>|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<string> $responses
*
* @return list<array{label:string,count:int,percentage:int}>
*/
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<array{background:string,border:string}>
*/
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;
}
}