feat: calmer questionnaire form with selects and clickable answer rows

This commit is contained in:
fromme committed 2026-10-27 17:32:41 +01:00
1 parent af97468b2d
commit 0b736b51fb
5 files changed
+65 -5

No files matched your search

+14
View File
@@ -35,6 +35,7 @@ sections:
- key: bus_company
label: "Bus-Unternehmen:"
type: choice
widget: dropdown
required: true
options:
- { key: boonk, label: "Boonk Reisen" }
@@ -80,6 +81,7 @@ sections:
- key: overall_impression
label: "Gesamteindruck des Busses"
type: choice
widget: dropdown
required: true
options:
- { key: very_good, label: "Sehr gut" }
@@ -91,6 +93,7 @@ sections:
- key: vehicle_age
label: "Alter des Fahrzeugs"
type: choice
widget: dropdown
required: true
options:
- { key: very_new, label: "Sehr neu" }
@@ -102,6 +105,7 @@ sections:
- key: seating_comfort
label: "Sitzkomfort im Bus"
type: choice
widget: dropdown
required: true
options:
- { key: very_good, label: "Sehr gut" }
@@ -113,6 +117,7 @@ sections:
- key: driver_friendliness
label: "Freundlichkeit der Fahrer"
type: choice
widget: dropdown
required: true
options:
- { key: very_friendly, label: "Sehr freundlich" }
@@ -124,6 +129,7 @@ sections:
- key: breaks
label: "Pausenzeiten"
type: choice
widget: dropdown
required: true
options:
- { key: very_good, label: "Sehr gut" }
@@ -135,6 +141,7 @@ sections:
- key: temperature
label: "Temperatur im Bus"
type: choice
widget: dropdown
required: true
options:
- { key: very_good, label: "Sehr gut" }
@@ -146,6 +153,7 @@ sections:
- key: guest_behaviour
label: "Verhalten der Gäste im Bus"
type: choice
widget: dropdown
required: true
options:
- { key: very_good, label: "Sehr gut" }
@@ -188,6 +196,7 @@ sections:
- key: video
label: "Video/DVD-Gerät"
type: choice
widget: dropdown
required: true
options:
- { key: working, label: "funktionstüchtig" }
@@ -197,6 +206,7 @@ sections:
- key: wc
label: "WC"
type: choice
widget: dropdown
required: true
options:
- { key: working, label: "funktionstüchtig" }
@@ -206,6 +216,7 @@ sections:
- key: luggage
label: "Gepäck"
type: choice
widget: dropdown
required: true
options:
- { key: trailer, label: "Anhänger" }
@@ -216,6 +227,7 @@ sections:
- key: smoking
label: "Rauchen im Bus"
type: choice
widget: dropdown
required: true
options:
- { key: driver, label: "Busfahrer hat im Bus geraucht" }
@@ -225,6 +237,7 @@ sections:
- key: snow_chains
label: "Schneeketten"
type: choice
widget: dropdown
required: true
options:
- { key: fitted, label: "Fahrer hat Ketten aufgezogen" }
@@ -237,6 +250,7 @@ sections:
- key: bus_damage
label: "Entstanden während der Fahrt Schäden am Bus?"
type: choice
widget: dropdown
required: true
options:
- { key: none, label: "Keine Schäden" }
@@ -42,6 +42,7 @@ sections:
- key: team_week_seminar
label: "War das Wochenteam-Seminar hilfreich für dich? Warst du bzw. wurdest du gut vorbereitet?"
type: choice
widget: dropdown
required: true
options:
- { key: very_helpful, label: "Ja, die Schulung war sehr hilfreich und ich fühlte mich bestens vorbereitet." }
@@ -84,6 +85,7 @@ sections:
- key: house_manager_problem_handling
label: "Wie ist deine Hausleitung mit Problemen vor Ort umgegangen?"
type: choice
widget: dropdown
required: true
options:
- { key: very_good, label: "Sehr gut – Probleme wurden schnell, fair und lösungsorientiert behandelt." }
+11 -5
View File
@@ -151,14 +151,20 @@ class QuestionnaireType extends AbstractType
return match ($question->type) {
QuestionType::TEXT => [TextType::class, $common],
QuestionType::TEXTAREA => [AutosizeTextareaType::class, $common + ['min_rows' => 3]],
QuestionType::CHOICE => [ChoiceType::class, $common + $this->choices($question) + [
'expanded' => ChoiceWidget::RADIO === $question->widget,
// a radio list needs no "none" entry, a select needs an empty first line
'placeholder' => ChoiceWidget::RADIO === $question->widget ? false : 'Bitte wählen',
]],
QuestionType::CHOICE => [ChoiceType::class, $common + $this->choices($question) + (ChoiceWidget::RADIO === $question->widget ? [
'expanded' => true,
// a radio list needs no "none" entry
'placeholder' => false,
'block_prefix' => 'questionnaire_choice',
] : [
'expanded' => false,
// a select needs an empty first line, or the first option would be preselected
'placeholder' => 'Bitte wählen',
])],
QuestionType::MULTI => [ChoiceType::class, $common + $this->choices($question) + [
'expanded' => true,
'multiple' => true,
'block_prefix' => 'questionnaire_choice',
]],
QuestionType::DATE => [DatepickerType::class, $common + [
'input' => 'string',
+17
View File
@@ -207,6 +207,23 @@
</fieldset>
{%- endblock questionnaire_section_row -%}
{#
# A questionnaire's radio or checkbox list: each answer a whole clickable row that lights up on
# hover and stays marked once chosen, so it is plain where to click. Questionnaire-only; the
# app's other choice lists keep choice_widget_expanded.
#}
{%- block questionnaire_choice_widget -%}
<div class="flex flex-col gap-y-1" {{ block('widget_container_attributes') }}>
{%- for child in form -%}
<label for="{{ child.vars.id }}"
class="{{ html_classes('flex cursor-pointer select-none items-center gap-x-3 rounded-md px-3 py-2 text-sm ring-1 ring-inset transition-colors hover:bg-gray-100 has-[:checked]:bg-primary/10 has-[:checked]:ring-primary', { 'ring-red-500': errors|length, 'ring-gray-200': not errors|length }) }}">
{{- form_widget(child, { attr: { class: 'h-4 w-4 shrink-0 border-gray-300 text-primary focus:ring-primary' } }) -}}
<span class="text-gray-900">{{ child.vars.label }}</span>
</label>
{%- endfor -%}
</div>
{%- endblock questionnaire_choice_widget -%}
{# A 1-5 rating as a row of buttons, the radios themselves hidden. #}
{%- block questionnaire_rating_widget -%}
<div class="flex gap-x-2" role="radiogroup">
+21
View File
@@ -59,6 +59,27 @@ class QuestionnaireTemplateTest extends KernelTestCase
$this->assertSame([$this->definition()->title, $this->busDefinition()->title], $headings);
}
/**
* Long scales are selects, starting empty; Ja/Nein stays radios, each answer a clickable row.
*/
public function testLongScalesAreSelectsAndYesNoAreClickableRows(): void
{
$crawler = $this->renderForm();
$seminar = $crawler->filter('[data-question-key="teamer_feedback.team_week_seminar"] select');
$this->assertCount(1, $seminar);
$this->assertSame('', $seminar->filter('option')->first()->attr('value'));
$this->assertCount(1, $crawler->filter('[data-question-key="bus_feedback.wc"] select'));
// the labels wrapping a radio: the whole row is the click target
$rows = $crawler
->filter('[data-question-key="teamer_feedback.had_organisational_problems"] label')
->reduce(static fn (Crawler $label): bool => 1 === $label->filter('input[type="radio"]')->count())
;
$this->assertCount(2, $rows);
$this->assertStringContainsString('hover:', $rows->first()->attr('class'));
}
public function testRatingIsFiveRadiosInARadioGroup(): void
{
$group = $this->renderForm()->filter('[data-question-key="teamer_feedback.office_briefing_rating"] [role="radiogroup"]');