feat: HEADING field type for registration forms — replace section property with structural field

Replace the per-field `section` text property with a dedicated HEADING field type that
organizers add as a separate block for visual grouping. Also fixes duplicate heading bug
on portal radio fields, replaces cramped VBtnToggle with VSelect for field width, and
adds grouped field type dropdown with structure/input categories.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-16 16:40:41 +02:00
parent 9718e27029
commit d57dcdb616
27 changed files with 667 additions and 480 deletions

View File

@@ -173,20 +173,6 @@ const currentStepKind = computed(() => {
const registrationFieldsList = computed(() => registrationData.value?.registration_fields ?? [])
const groupedRegistrationFields = computed(() => {
const fields = registrationFieldsList.value
const groups: { section: string | null; fields: RegistrationField[] }[] = []
for (const f of fields) {
const last = groups[groups.length - 1]
if (last && last.section === f.section)
last.fields.push(f)
else
groups.push({ section: f.section, fields: [f] })
}
return groups
})
function defaultFieldValue(type: RegistrationFieldType): unknown {
switch (type) {
case 'multiselect':
@@ -196,6 +182,7 @@ function defaultFieldValue(type: RegistrationFieldType): unknown {
case 'boolean':
return false
case 'number':
case 'heading':
return null
default:
return ''
@@ -204,6 +191,7 @@ function defaultFieldValue(type: RegistrationFieldType): unknown {
watch(registrationFieldsList, fields => {
for (const f of fields) {
if (f.field_type === 'heading') continue
if (!(f.slug in fieldFormData.value))
fieldFormData.value[f.slug] = defaultFieldValue(f.field_type)
}
@@ -337,7 +325,7 @@ function validateDynamicFields(): boolean {
fieldErrors.value = {}
let firstErrorSlug: string | null = null
for (const field of registrationFieldsList.value) {
if (!field.is_required) continue
if (field.field_type === 'heading' || !field.is_required) continue
const val = fieldFormData.value[field.slug]
if (isEmptyFieldValue(val, field.field_type)) {
fieldErrors.value[field.slug] = 'Dit veld is verplicht.'
@@ -434,6 +422,7 @@ function formatTimeRange(start: string, end: string): string {
function buildFieldValuesPayload(): Record<string, unknown> | undefined {
const out: Record<string, unknown> = {}
for (const field of registrationFieldsList.value) {
if (field.field_type === 'heading') continue
const val = fieldFormData.value[field.slug]
if (field.field_type === 'boolean') {
if (typeof val === 'boolean')
@@ -1047,21 +1036,32 @@ async function onSubmit() {
<!-- Step 1: Extra informatie (dynamic registration_fields) -->
<div v-show="currentStep === 1">
<template
v-for="(group, gi) in groupedRegistrationFields"
:key="gi"
>
<div
v-if="group.section"
class="text-subtitle-2 text-medium-emphasis mt-4 mb-2"
>
{{ group.section }}
</div>
<VRow>
<VCol
v-for="field in group.fields"
<template
v-for="(field, fieldIndex) in registrationFieldsList"
:key="field.id"
>
<!-- HEADING field: full-width section header -->
<VCol
v-if="field.field_type === 'heading'"
cols="12"
:class="fieldIndex === 0 ? '' : 'mt-4'"
>
<div class="text-subtitle-1 font-weight-medium mb-1">
{{ field.label }}
</div>
<div
v-if="field.help_text"
class="text-body-2 text-medium-emphasis mb-3"
>
{{ field.help_text }}
</div>
<VDivider class="mb-2" />
</VCol>
<!-- Regular input field -->
<VCol
v-else
cols="12"
:md="field.display_width === 'half' ? 6 : 12"
>
@@ -1270,8 +1270,8 @@ async function onSubmit() {
/>
</div>
</VCol>
</template>
</VRow>
</template>
<p
v-if="registrationFieldsList.length === 0"

View File

@@ -8,6 +8,7 @@ export type RegistrationFieldType =
| 'boolean'
| 'number'
| 'tag_picker'
| 'heading'
export type FieldDisplayWidth = 'full' | 'half'
@@ -31,7 +32,6 @@ export interface RegistrationField {
normalized_options: NormalizedOption[] | null
tag_category: string | null
is_required: boolean
section: string | null
help_text: string | null
display_width: FieldDisplayWidth
available_tags?: RegistrationTagOption[]