Files
survey-bundle/contao/templates/frontend/show_survey_results.html.twig
T
admin.juergenandClaude Fable 5 6077832efc Honor the mandatory flag with explicit no-answer options
Symfony's required default forced an answer to every question no matter
what the mandatory checkbox said. Optional choice questions now offer a
dedicated 'Keine Antwort' option, optional range and text questions a
'Keine Angaben machen' checkbox (preselected for ranges, because a slider
always submits a value); the side that is not chosen gets dimmed but stays
clickable. Mandatory ranges start their value display at '?' and reject
the submit until the slider was actually used, so the browser's midpoint
default no longer leaks into the results.

Empty answers are stored as a deliberate 'no answer', kept out of counts,
averages and charts, and reported separately on the results page and in
the PDF export. Also gives the reader textarea its full width and field
styling, adds grid overflow guards for unbreakable content and styles the
form error box.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-26 14:36:38 +02:00

172 lines
9.7 KiB
Twig

{% trans_default_domain 'messages' %}
{% include '@Survey/frontend/_survey_assets.html.twig' %}
{% include '@Survey/frontend/_survey_branding.html.twig' %}
{% set moduleHeadline = (headline is iterable ? headline.text|default('') : headline|default(''))|striptags|trim %}
<section class="survey-shell survey-grid">
{% if moduleHeadline %}
<header>
<h2>{{ moduleHeadline }}</h2>
</header>
{% endif %}
{% if errorMessage %}
<div class="survey-alert warning">{{ errorMessage }}</div>
{% elseif survey %}
<article class="survey-card">
<div class="survey-meta">
<h2>{{ survey.title|striptags|trim }}</h2>
<span class="survey-badge neutral">{{ 'survey.results.participations'|trans({'%count%': completedSubmissionCount}) }}</span>
<span class="survey-badge neutral">{{ 'survey.results.questions'|trans({'%count%': questionResults|length}) }}</span>
</div>
{% if survey.description|survey_has_text %}
<div class="survey-rich-text">{{ survey.description|survey_rich }}</div>
{% else %}
<p>{{ 'survey.results.no_description'|trans }}</p>
{% endif %}
{% if downloadUrl or pdfDownloadUrl %}
<div class="survey-button-row">
{% if downloadUrl %}
<a class="survey-button primary" href="{{ downloadUrl }}">{{ 'survey.results.download_excel'|trans }}</a>
{% endif %}
{% if pdfDownloadUrl %}
<a class="survey-button secondary" href="{{ pdfDownloadUrl }}">{{ 'survey.results.download_pdf'|trans }}</a>
{% endif %}
</div>
{% endif %}
</article>
<div class="survey-grid">
{% set previousSectionId = null %}
{% for question in questionResults %}
{% set sectionId = question.section.id|default(0) %}
{% if hasSections|default(false) and sectionId != previousSectionId %}
<header class="survey-results-section-head">
<span class="survey-section-kicker">{{ 'survey.results.section_kicker'|trans }}</span>
<h3>{{ question.section.title|default('') ?: 'survey.results.section_default'|trans }}</h3>
</header>
{% set previousSectionId = sectionId %}
{% endif %}
<article class="survey-card survey-results-card">
<div class="survey-meta">
<h3>{{ question.question|striptags|trim }}</h3>
<span class="survey-badge neutral">{{ 'survey.results.answered'|trans({'%count%': question.totalAnswers, '%total%': completedSubmissionCount}) }}</span>
{% if question.noAnswerCount|default(0) > 0 %}
<span class="survey-badge neutral">{{ 'survey.results.no_answer'|trans({'%count%': question.noAnswerCount}) }}</span>
{% endif %}
{% if question.skippedCount > 0 %}
<span class="survey-badge neutral">{{ 'survey.results.skipped'|trans({'%count%': question.skippedCount}) }}</span>
{% endif %}
</div>
{% if question.description|survey_has_text %}
<div class="survey-rich-text">{{ question.description|survey_rich }}</div>
{% endif %}
<div class="survey-results-layout{% if not question.chart %} no-chart{% endif %}">
{% if question.chart %}
<div class="survey-chart-panel{% if question.chart.type == 'line' %} is-line{% endif %}">
<div class="survey-chart-frame">
<canvas class="survey-chart-canvas" data-survey-chart="{{ question.chart|json_encode|e('html_attr') }}"></canvas>
</div>
</div>
{% endif %}
<div class="survey-results-panel">
{% if question.type in ['yes_no_maybe', 'choice'] %}
<div class="survey-results-list">
{% for option in question.options %}
<div class="survey-results-row">
<div class="survey-meta">
<strong>{{ option.label|striptags|trim }}</strong>
<span class="survey-badge neutral">{{ option.count }}</span>
<span class="survey-badge neutral">{{ option.percentage }}%</span>
</div>
<div class="survey-progress">
<span class="survey-progress-bar" style="width: {{ option.percentage }}%"></span>
</div>
</div>
{% endfor %}
</div>
{% elseif question.type == 'range' %}
<div class="survey-metric-grid">
<div class="survey-metric-card">
<span class="survey-metric-label">{{ 'survey.results.minimum'|trans }}</span>
<strong>{{ question.minimum is not null ? question.minimum : '-' }}</strong>
</div>
<div class="survey-metric-card">
<span class="survey-metric-label">{{ 'survey.results.maximum'|trans }}</span>
<strong>{{ question.maximum is not null ? question.maximum : '-' }}</strong>
</div>
<div class="survey-metric-card">
<span class="survey-metric-label">{{ 'survey.results.average'|trans }}</span>
<strong>{{ question.average is not null ? question.average : '-' }}</strong>
</div>
</div>
<div class="survey-results-list">
{% for option in question.distributionRows|default([]) %}
<div class="survey-results-row">
<div class="survey-meta">
<strong>{{ option.label|striptags|trim }}</strong>
<span class="survey-badge neutral">{{ 'survey.results.answer_count'|trans({'%count%': option.count}) }}</span>
<span class="survey-badge neutral">{{ option.percentage }}%</span>
</div>
<div class="survey-progress">
<span class="survey-progress-bar" style="width: {{ option.percentage }}%"></span>
</div>
</div>
{% else %}
<p>{{ 'survey.results.no_ratings'|trans }}</p>
{% endfor %}
</div>
{% else %}
{# Eine Liste statt Doppel-Auflistung: jede Antwort einmal,
mit exakter Anzahl statt Prozentwerten. #}
<div class="survey-text-response-grid survey-results-responses">
{% for option in question.responseSummary|default([]) %}
<article class="survey-card survey-response-card">
<span class="survey-badge neutral">{{ 'survey.results.answer_count'|trans({'%count%': option.count}) }}</span>
<blockquote>{{ option.label|striptags|trim|nl2br }}</blockquote>
</article>
{% else %}
<p>{{ 'survey.results.no_text_responses'|trans }}</p>
{% endfor %}
</div>
{% endif %}
</div>
</div>
</article>
{% else %}
<article class="survey-card">{{ 'survey.results.no_questions'|trans }}</article>
{% endfor %}
</div>
{% endif %}
</section>
<script src="{{ asset('bundles/survey/js/chart.umd.min.js') }}"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
if (typeof Chart === 'undefined') {
return;
}
document.querySelectorAll('[data-survey-chart]').forEach(function (canvas) {
try {
var chartConfig = JSON.parse(canvas.getAttribute('data-survey-chart') || '{}');
if (!chartConfig.type || !chartConfig.data) {
return;
}
new Chart(canvas, chartConfig);
} catch (error) {
console.error('Survey chart could not be initialized.', error);
}
});
});
</script>