2 Commits
Author SHA1 Message Date
admin.juergenandClaude Fable 5 fa0bbaa73c Default new questions to optional and add opt-in thumb icons for yes/no
Two customer decisions from the September feedback round:

- New questions are now optional by default; the mandatory checkbox is
  ticked deliberately (backend DCA default and frontend editor model).
  Existing questions, templates and duplication are unaffected.
- Yes/no/maybe questions get a per-question display option 'Antworten
  als Daumen darstellen' (showThumbs, e.g. for surveys with children):
  the reader renders thumb-up/down/sideways SVG icons above the answer
  labels. Purely presentational - answers are still stored, evaluated
  and exported as yes/no/maybe, and the 'Keine Antwort' card, jump
  hints and dim logic are unchanged. Icons are inline SVG in CI colors
  (no CDN). Requires contao:migrate for the new column.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-11 11:38:55 +02:00
admin.juergenandClaude Fable 5 ab2dbfbc45 Add a restart button to the survey draft view
Pretesters had no way to leave or restart the draft view other than
clicking through every remaining question (customer pretest finding
no. 2). The preview keeps its state exclusively in hidden POST fields,
so a plain GET on the preview URL starts over at question 1 - the new
'Umfragenentwurf von vorn beginnen' button in the preview notice bar
links exactly there and is visible on every draft page including the
completion page. Jump marks remain deliberately unsupported so the
draft view keeps mirroring the real participation flow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-08 15:28:00 +02:00
13 changed files with 74 additions and 4 deletions

No files matched your search

+9 -2
View File
@@ -73,7 +73,7 @@ $GLOBALS['TL_DCA']['tl_survey_content'] = [
],
'palettes' => [
'default' => '{question_legend},type,question,description;{settings_legend},mandatory,published',
'yes_no_maybe' => '{question_legend},type,question,description;{settings_legend},mandatory,published,allowMaybe,jumpOnYes,jumpOnNo',
'yes_no_maybe' => '{question_legend},type,question,description;{settings_legend},mandatory,published,allowMaybe,showThumbs,jumpOnYes,jumpOnNo',
'text' => '{question_legend},type,question,description;{settings_legend},mandatory,published',
'range' => '{question_legend},type,question,description;{settings_legend},mandatory,published,rangeMin,rangeMax,rangeStep',
'choice' => '{question_legend},type,question,description;{settings_legend},mandatory,published,allowMultiple,answerOption1,answerOption2,answerOption3,answerOption4,answerOption5,answerOption6,answerOption7,answerOption8,answerOption9,answerOption10',
@@ -111,7 +111,8 @@ $GLOBALS['TL_DCA']['tl_survey_content'] = [
'mandatory' => [
'label' => &$GLOBALS['TL_LANG']['tl_survey_content']['mandatory'],
'inputType' => 'checkbox',
'default' => '1',
// Neue Fragen sind bewusst freiwillig; Pflicht wird aktiv angehakt
// (Kundenentscheidung 09/2026, Pretest-Punkt 3).
'eval' => ['tl_class' => 'w50 m12'],
'sql' => "char(1) NOT NULL default ''",
],
@@ -128,6 +129,12 @@ $GLOBALS['TL_DCA']['tl_survey_content'] = [
'eval' => ['tl_class' => 'w50 m12 clr'],
'sql' => "char(1) NOT NULL default ''",
],
'showThumbs' => [
'label' => &$GLOBALS['TL_LANG']['tl_survey_content']['showThumbs'],
'inputType' => 'checkbox',
'eval' => ['tl_class' => 'w50 m12'],
'sql' => "char(1) NOT NULL default ''",
],
'jumpOnYes' => [
'label' => &$GLOBALS['TL_LANG']['tl_survey_content']['jumpOnYes'],
'inputType' => 'select',
@@ -15,6 +15,7 @@ $GLOBALS['TL_LANG']['tl_survey_content']['settings_legend'] = 'Einstellungen';
$GLOBALS['TL_LANG']['tl_survey_content']['mandatory'] = ['Pflichtfrage', 'Diese Frage muss beantwortet werden.'];
$GLOBALS['TL_LANG']['tl_survey_content']['published'] = ['Aktiv', 'Die Frage wird in der Umfrage angezeigt.'];
$GLOBALS['TL_LANG']['tl_survey_content']['allowMaybe'] = ['Vielleicht anbieten', 'Ergänzt die Ja-Nein-Frage um die Antwortmöglichkeit Vielleicht.'];
$GLOBALS['TL_LANG']['tl_survey_content']['showThumbs'] = ['Antworten als Daumen darstellen', 'Zeigt bei Ja/Nein/Vielleicht zusätzlich Daumen-Symbole an (z. B. für Befragungen mit Kindern). Gespeichert und ausgewertet werden weiterhin die Antworten Ja, Nein und Vielleicht.'];
$GLOBALS['TL_LANG']['tl_survey_content']['jumpOnYes'] = ['Zur Frage springen bei Auswahl ja', 'Springt bei der Antwort Ja direkt zu dieser Frage.'];
$GLOBALS['TL_LANG']['tl_survey_content']['jumpOnNo'] = ['Zur Frage springen bei Auswahl nein', 'Springt bei der Antwort Nein direkt zu dieser Frage.'];
$GLOBALS['TL_LANG']['tl_survey_content']['allowMultiple'] = ['Multiple-Choice aktivieren', 'Erlaubt mehrere Antworten statt einer einzelnen Auswahl.'];
@@ -15,6 +15,7 @@ $GLOBALS['TL_LANG']['tl_survey_content']['settings_legend'] = 'Settings';
$GLOBALS['TL_LANG']['tl_survey_content']['mandatory'] = ['Required question', 'This question must be answered.'];
$GLOBALS['TL_LANG']['tl_survey_content']['published'] = ['Active', 'The question is shown in the survey.'];
$GLOBALS['TL_LANG']['tl_survey_content']['allowMaybe'] = ['Offer maybe', 'Adds the Maybe answer option to the yes-no question.'];
$GLOBALS['TL_LANG']['tl_survey_content']['showThumbs'] = ['Show thumb icons', 'Displays thumb icons for yes/no/maybe answers (e.g. for surveys with children). Answers are still stored and evaluated as yes, no and maybe.'];
$GLOBALS['TL_LANG']['tl_survey_content']['jumpOnYes'] = ['Jump to question on yes', 'Jumps directly to this question when the answer is Yes.'];
$GLOBALS['TL_LANG']['tl_survey_content']['jumpOnNo'] = ['Jump to question on no', 'Jumps directly to this question when the answer is No.'];
$GLOBALS['TL_LANG']['tl_survey_content']['allowMultiple'] = ['Enable multiple choice', 'Allows multiple answers instead of a single selection.'];
@@ -31,6 +31,7 @@
<label class="survey-inline-check">{{ form_widget(form.mandatory) }}<span>{{ 'survey.edit.mandatory'|trans }}</span></label>
<label class="survey-inline-check">{{ form_widget(form.published) }}<span>{{ 'survey.edit.active'|trans }}</span></label>
<label class="survey-inline-check yes-no-config-field"{% if questionType != 'yes_no_maybe' %} style="display:none"{% endif %}>{{ form_widget(form.allowMaybe) }}<span>{{ 'survey.edit.allow_maybe'|trans }}</span></label>
<label class="survey-inline-check yes-no-config-field"{% if questionType != 'yes_no_maybe' %} style="display:none"{% endif %}>{{ form_widget(form.showThumbs) }}<span>{{ 'survey.edit.show_thumbs'|trans }}</span></label>
<label class="survey-inline-check choice-config-field"{% if questionType != 'choice' %} style="display:none"{% endif %}>{{ form_widget(form.allowMultiple) }}<span>{{ 'survey.edit.allow_multiple'|trans }}</span></label>
</div>
<div class="yes-no-config-field survey-grid-columns" style="grid-template-columns: repeat(2, minmax(0, 1fr));{% if questionType != 'yes_no_maybe' %}display:none;{% endif %}">
@@ -30,7 +30,10 @@
{% endif %}
{% if isPreview %}
<div class="survey-alert warning">{{ 'survey.survey.preview_notice'|trans }}</div>
<div class="survey-alert warning survey-preview-bar">
<span>{{ 'survey.survey.preview_notice'|trans }}</span>
<a class="survey-button secondary survey-preview-restart" href="{{ previewRestartUrl }}">{{ 'survey.survey.preview_restart'|trans }}</a>
</div>
{% endif %}
{% if progress.total > 0 %}
@@ -76,9 +79,16 @@
{% set answerChildren = surveyForm.answer is defined ? surveyForm.answer|filter(child => child.vars.value is not defined or child.vars.value != '__no_answer__') : [] %}
{% set noAnswerChildren = surveyForm.answer is defined ? surveyForm.answer|filter(child => child.vars.value is defined and child.vars.value == '__no_answer__') : [] %}
{% if question.type == 'yes_no_maybe' %}
{# Reine Darstellungsoption: gespeichert wird weiterhin yes/no/maybe. #}
{% set showThumbIcons = '1' == question.showThumbs %}
<div class="survey-grid" style="grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));" data-survey-answer-zone>
{% for child in answerChildren %}
<label class="survey-card" style="cursor: pointer;">
{% if showThumbIcons and child.vars.value in ['yes', 'no', 'maybe'] %}
<span class="survey-thumb survey-thumb--{{ child.vars.value }}" aria-hidden="true">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" focusable="false"><path fill="currentColor" d="M2 20h2c.55 0 1-.45 1-1v-9c0-.55-.45-1-1-1H2v11zm19.83-7.12c.11-.25.17-.52.17-.8V11c0-1.1-.9-2-2-2h-5.5l.92-4.65c.05-.22.02-.46-.08-.66-.23-.45-.52-.86-.88-1.22L14 2 7.59 8.41C7.21 8.79 7 9.3 7 9.83v7.84C7 18.95 8.05 20 9.34 20h8.11c.7 0 1.36-.37 1.72-.97l2.66-6.15z"/></svg>
</span>
{% endif %}
<div class="survey-meta"><strong>{{ child.vars.label|striptags|trim }}</strong>{{ form_widget(child) }}</div>
{% set jumpHint = jumpHints|default({})[child.vars.value]|default(null) %}
{% if jumpHint %}
+29
View File
@@ -727,6 +727,35 @@
.survey-alert.error { border-color: rgba(157, 82, 118, 0.28); color: var(--survey-purple); }
.survey-alert.warning { border-color: rgba(236, 124, 50, 0.28); color: var(--survey-orange); }
.survey-preview-bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.8rem;
flex-wrap: wrap;
}
/* Optionale Daumen-Darstellung bei Ja/Nein/Vielleicht (z. B. Kinderbefragungen).
Reine Anzeige – gespeichert wird weiterhin yes/no/maybe. */
.survey-thumb {
display: flex;
justify-content: center;
margin: 0.2rem auto 0.55rem;
}
.survey-thumb svg { width: 46px; height: 46px; }
.survey-thumb--yes { color: var(--survey-green); }
.survey-thumb--no { color: var(--survey-orange); }
.survey-thumb--maybe { color: var(--survey-gray-dark); }
.survey-thumb--no svg { transform: rotate(180deg); }
.survey-thumb--maybe svg { transform: rotate(90deg); }
.survey-preview-bar .survey-preview-restart {
flex-shrink: 0;
padding: 0.45rem 0.95rem;
font-size: 0.85rem;
}
.survey-kpis {
display: grid;
gap: 1rem;
@@ -170,6 +170,10 @@ final class ShowSurveyController extends AbstractFrontendModuleController
private function buildPreviewResponse(FragmentTemplate $template, SurveyModel $survey, Request $request): Response
{
// Die Entwurfsansicht hält ihren Zustand nur in POST-Feldern – ein einfacher
// GET auf dieselbe URL beginnt daher wieder bei Frage 1 ("von vorn beginnen").
$template->set('previewRestartUrl', $request->getUri());
$history = $this->decodePreviewHistory((string) $request->request->get('preview_history', ''));
$question = $this->resolvePreviewQuestion($survey, (int) $request->request->get('preview_question', 0));
+6 -1
View File
@@ -11,9 +11,12 @@ final class SurveyQuestionData
public string $type = 'yes_no_maybe';
public string $question = '';
public ?string $description = '';
public bool $mandatory = true;
// Neue Fragen sind bewusst freiwillig; Pflicht wird aktiv angehakt
// (Kundenentscheidung 09/2026, Pretest-Punkt 3).
public bool $mandatory = false;
public bool $published = true;
public bool $allowMaybe = false;
public bool $showThumbs = false;
public bool $allowMultiple = false;
public ?int $jumpOnYes = 0;
public ?int $jumpOnNo = 0;
@@ -46,6 +49,7 @@ final class SurveyQuestionData
$data->mandatory = '1' === (string) $question->mandatory;
$data->published = '1' === (string) $question->published;
$data->allowMaybe = '1' === (string) $question->allowMaybe;
$data->showThumbs = '1' === (string) $question->showThumbs;
$data->allowMultiple = '1' === (string) $question->allowMultiple;
$data->jumpOnYes = (int) $question->jumpOnYes;
$data->jumpOnNo = (int) $question->jumpOnNo;
@@ -74,6 +78,7 @@ final class SurveyQuestionData
'mandatory' => $this->mandatory,
'published' => $this->published,
'allowMaybe' => $this->allowMaybe,
'showThumbs' => $this->showThumbs,
'allowMultiple' => $this->allowMultiple,
'jumpOnYes' => max(0, (int) ($this->jumpOnYes ?? 0)),
'jumpOnNo' => max(0, (int) ($this->jumpOnNo ?? 0)),
+5
View File
@@ -71,6 +71,11 @@ final class SurveyQuestionEditorType extends AbstractType
'required' => false,
'attr' => ['data-question-editor-target' => 'yesNoMaybeField'],
])
->add('showThumbs', CheckboxType::class, [
'label' => 'Antworten als Daumen darstellen',
'required' => false,
'attr' => ['data-question-editor-target' => 'yesNoMaybeField'],
])
->add('jumpOnYes', ChoiceType::class, [
'label' => 'Zur Frage springen bei Auswahl ja',
'required' => false,
@@ -298,6 +298,7 @@ final class SurveyQuestionRepository
'mandatory' => !$isSection && !empty($data['mandatory']) ? '1' : '',
'published' => $isSection || !empty($data['published']) ? '1' : '',
'allowMaybe' => $isYesNo && !empty($data['allowMaybe']) ? '1' : '',
'showThumbs' => $isYesNo && !empty($data['showThumbs']) ? '1' : '',
'jumpOnYes' => $isYesNo ? max(0, (int) ($data['jumpOnYes'] ?? $existing?->jumpOnYes ?? 0)) : 0,
'jumpOnNo' => $isYesNo ? max(0, (int) ($data['jumpOnNo'] ?? $existing?->jumpOnNo ?? 0)) : 0,
'allowMultiple' => $isChoice && !empty($data['allowMultiple']) ? '1' : '',
+2
View File
@@ -153,6 +153,7 @@ final class SurveyEditorService
'mandatory' => '1' === (string) $sourceQuestion->mandatory,
'published' => '1' === (string) $sourceQuestion->published,
'allowMaybe' => '1' === (string) ($sourceQuestion->allowMaybe ?? ''),
'showThumbs' => '1' === (string) ($sourceQuestion->showThumbs ?? ''),
'allowMultiple' => '1' === (string) ($sourceQuestion->allowMultiple ?? ''),
'answerOption1' => (string) ($sourceQuestion->answerOption1 ?? ''),
'answerOption2' => (string) ($sourceQuestion->answerOption2 ?? ''),
@@ -191,6 +192,7 @@ final class SurveyEditorService
'mandatory' => '1' === (string) $sourceQuestion->mandatory,
'published' => '1' === (string) $sourceQuestion->published,
'allowMaybe' => '1' === (string) ($sourceQuestion->allowMaybe ?? ''),
'showThumbs' => '1' === (string) ($sourceQuestion->showThumbs ?? ''),
'allowMultiple' => '1' === (string) ($sourceQuestion->allowMultiple ?? ''),
'answerOption1' => (string) ($sourceQuestion->answerOption1 ?? ''),
'answerOption2' => (string) ($sourceQuestion->answerOption2 ?? ''),
+2
View File
@@ -88,6 +88,7 @@ survey:
mandatory: "Pflichtfrage"
active: "Aktiv"
allow_maybe: "Vielleicht anbieten"
show_thumbs: "Antworten als Daumen darstellen"
jump_yes: "Zur Frage springen bei Auswahl ja"
jump_no: "Zur Frage springen bei Auswahl nein"
allow_multiple: "Multiple-Choice aktivieren"
@@ -145,6 +146,7 @@ survey:
next: "Weiter"
preview_badge: "Entwurf"
preview_notice: "Dies ist eine Entwurfsansicht. Antworten können getestet werden, werden aber nicht gespeichert."
preview_restart: "Umfragenentwurf von vorn beginnen"
preview_thanks_text: "Der Testlauf ist abgeschlossen. Antworten wurden nicht gespeichert."
results:
section_kicker: "Themenbereich"
+2
View File
@@ -88,6 +88,7 @@ survey:
mandatory: "Required question"
active: "Active"
allow_maybe: "Offer maybe"
show_thumbs: "Show thumb icons"
jump_yes: "Jump to question on yes"
jump_no: "Jump to question on no"
allow_multiple: "Enable multiple choice"
@@ -145,6 +146,7 @@ survey:
next: "Next"
preview_badge: "Draft"
preview_notice: "This is a draft view. Answers can be tested, but they will not be stored."
preview_restart: "Restart the survey draft"
preview_thanks_text: "The test run is complete. Answers were not stored."
results:
section_kicker: "Topic section"