c7439e4822
Behebt die Fehler aus der Kunden-Testrunde (Praktikantin, Juli 2026):
- Ergebnisreihenfolge: Fragen werden vor dem Finalisieren nach _sorting
sortiert. finalizeQuestionResult entfernt _sorting, daher fiel die
bisherige Nachsortierung auf den alphabetischen Fallback zurück. Online,
PDF und Excel folgen jetzt derselben Frage-Sortierung.
- Excel-Export: Eigene Fragetext-Zeile über den Antwortspalten (je Frage
über die zugehörigen Spalten zusammengeführt). Zuvor fehlte der Fragetext.
- Range-Ergebnis: Verteilung deckt die komplette Skala rangeMin..rangeMax in
rangeStep-Schritten ab, damit die X-Achse linear und vollständig ist
(vorher nur tatsächlich vorkommende Werte in gleichen Abständen).
- Teilnahmen/Rücklauf pro Frage als absolute Zahlen ("X von Y beantwortet",
"Z übersprungen") statt reiner Prozent-Rücklaufquote.
- Freitextantworten gruppiert: jede Antwort einmal mit exakter Häufigkeit,
ohne Prozentangaben und ohne Doppel-Auflistung.
- Frontend-Liste: "Link kopieren"-Button (absolute Reader-URL) für
veröffentlichte Umfragen; Aktiv-Umschalter mit Textlabel statt nur Punkt;
Aktionslinks öffnen im selben Tab statt in neuen Tabs.
- Duplizieren: Titel-Kollisionsprüfung ohne Ausschluss der Quell-ID, damit
aus "... - Kopie" korrekt "... - Kopie 2" wird statt eines Dubletten-Titels.
Der internalNote-Fehler beim Anlegen/Duplizieren war ein fehlendes
DB-Schema-Update (Spalte tl_survey.internalNote) und wurde per
contao:migrate auf der Testumgebung nachgezogen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
156 lines
8.6 KiB
Twig
156 lines
8.6 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>
|
|
|
|
<p>{{ (survey.description|striptags|trim) ?: 'survey.results.no_description'|trans }}</p>
|
|
|
|
{% 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">
|
|
{% for question in questionResults %}
|
|
<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.skippedCount > 0 %}
|
|
<span class="survey-badge neutral">{{ 'survey.results.skipped'|trans({'%count%': question.skippedCount}) }}</span>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% if question.description|striptags|trim %}
|
|
<p>{{ question.description|striptags|trim }}</p>
|
|
{% 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.distribution %}
|
|
<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>
|
|
{% 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="https://cdn.jsdelivr.net/npm/chart.js@4.5.1/dist/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> |