Fix customer-reported survey results and list issues

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>
This commit is contained in:
2026-07-05 19:30:34 +02:00
parent 788f5ccde0
commit c7439e4822
11 changed files with 242 additions and 80 deletions
@@ -41,8 +41,10 @@
<article class="survey-card survey-results-card">
<div class="survey-meta">
<h3>{{ question.question|striptags|trim }}</h3>
<span class="survey-badge neutral">{{ 'survey.results.participations'|trans({'%count%': completedSubmissionCount}) }}</span>
<span class="survey-badge neutral">{{ 'survey.results.response_rate'|trans({'%rate%': question.responseRate}) }}</span>
<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 %}
@@ -107,25 +109,13 @@
{% endfor %}
</div>
{% else %}
{% if question.responseSummary is defined and question.responseSummary %}
<div class="survey-text-summary-grid">
{% for option in question.responseSummary %}
<article class="survey-text-summary-card">
<strong>{{ option.label|striptags|trim }}</strong>
<div class="survey-meta">
<span class="survey-badge neutral">{{ option.count }}</span>
<span class="survey-badge neutral">{{ option.percentage }}%</span>
</div>
</article>
{% endfor %}
</div>
{% endif %}
{# Eine Liste statt Doppel-Auflistung: jede Antwort einmal,
mit exakter Anzahl statt Prozentwerten. #}
<div class="survey-text-response-grid survey-results-responses">
{% for response in question.responses %}
{% for option in question.responseSummary|default([]) %}
<article class="survey-card survey-response-card">
<span class="survey-response-card__index">{{ loop.index }}</span>
<blockquote>{{ response|striptags|trim|nl2br }}</blockquote>
<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>