Refine survey editor workflow and publication handling

This commit is contained in:
Jürgen Mummert
2026-05-29 10:19:19 +02:00
parent 6bbded4181
commit 1174410c1b
40 changed files with 1121 additions and 335 deletions
+174 -101
View File
@@ -41,118 +41,191 @@ final class SurveyResultsViewService
/**
* @param list<SurveyContentModel> $questions
* @param list<array{questionId:int,value:string}> $answers
* @param list<array{questionId:int,questionType:string,questionLabel:string,questionDescription:string,questionSorting: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;
$key = $this->buildQuestionResultKey(
(int) $question->id,
(string) $question->type,
(string) $question->question,
(string) ($question->description ?? ''),
);
$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;
$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);
}
return $results;
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, $questionLabel, $questionDescription);
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 $questionLabel, string $questionDescription): string
{
return implode('|', [$questionId, $questionType, md5($questionLabel), md5($questionDescription)]);
}
/**
* @return array<string, mixed>
*/
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' => [],
];
}
/**
* @param array<string, mixed> $result
*
* @return array<string, mixed>
*/
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];
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']);
return $result;
}
/**
* @return list<string>
*/
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;
}
/**