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:
@@ -21,7 +21,55 @@ function formatOptions(options: NormalizedOption[] | null): string {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Heading variant -->
|
||||
<div
|
||||
v-if="field.field_type === 'heading'"
|
||||
class="d-flex align-center rounded pa-3 mb-2"
|
||||
style="background: rgba(var(--v-theme-on-surface), 0.04);"
|
||||
>
|
||||
<div class="drag-handle d-flex align-center cursor-grab me-2">
|
||||
<VIcon
|
||||
icon="tabler-grip-vertical"
|
||||
size="20"
|
||||
class="text-disabled"
|
||||
/>
|
||||
</div>
|
||||
<VIcon
|
||||
icon="tabler-heading"
|
||||
size="18"
|
||||
class="me-2 text-medium-emphasis"
|
||||
/>
|
||||
<div class="flex-grow-1 min-width-0">
|
||||
<div class="text-subtitle-2 font-weight-medium">
|
||||
{{ field.label }}
|
||||
</div>
|
||||
<div
|
||||
v-if="field.help_text"
|
||||
class="text-caption text-medium-emphasis"
|
||||
>
|
||||
{{ field.help_text }}
|
||||
</div>
|
||||
</div>
|
||||
<VBtn
|
||||
icon="tabler-edit"
|
||||
variant="text"
|
||||
size="small"
|
||||
title="Bewerken"
|
||||
@click="$emit('edit', field)"
|
||||
/>
|
||||
<VBtn
|
||||
icon="tabler-trash"
|
||||
variant="text"
|
||||
size="small"
|
||||
color="error"
|
||||
title="Verwijderen"
|
||||
@click="$emit('delete', field)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Regular field variant -->
|
||||
<VCard
|
||||
v-else
|
||||
variant="outlined"
|
||||
class="mb-2"
|
||||
>
|
||||
@@ -51,14 +99,6 @@ function formatOptions(options: NormalizedOption[] | null): string {
|
||||
</VChip>
|
||||
</div>
|
||||
|
||||
<!-- Section -->
|
||||
<div
|
||||
v-if="field.section"
|
||||
class="text-body-2 text-medium-emphasis mb-1"
|
||||
>
|
||||
Sectie: {{ field.section }}
|
||||
</div>
|
||||
|
||||
<!-- Options preview -->
|
||||
<div
|
||||
v-if="field.normalized_options?.length"
|
||||
|
||||
@@ -4,7 +4,7 @@ import { usePersonTagCategories } from '@/composables/api/usePersonTags'
|
||||
import { requiredValidator } from '@core/utils/validators'
|
||||
import type { RegistrationFormField, RegistrationFormFieldCreateDTO } from '@/types/registration-form-field'
|
||||
import type { RegistrationFieldType, FieldDisplayWidth } from '@/types/registration-field-template'
|
||||
import { FIELD_TYPE_LABELS, FIELD_TYPES_WITH_OPTIONS } from '@/types/registration-field-template'
|
||||
import { FIELD_TYPES_WITH_OPTIONS } from '@/types/registration-field-template'
|
||||
import type { AxiosError } from 'axios'
|
||||
import type { ApiErrorResponse } from '@/types/auth'
|
||||
|
||||
@@ -24,7 +24,21 @@ const modelValue = defineModel<boolean>({ default: false })
|
||||
const orgIdRef = computed(() => props.orgId)
|
||||
const { data: tagCategories } = usePersonTagCategories(orgIdRef)
|
||||
|
||||
const fieldTypeOptions = Object.entries(FIELD_TYPE_LABELS).map(([value, title]) => ({ title, value }))
|
||||
const fieldTypeItems = [
|
||||
{ type: 'subheader', title: 'Structuur' },
|
||||
{ title: 'Kop / Sectie-titel', value: 'heading', props: { prependIcon: 'tabler-heading' } },
|
||||
{ type: 'divider' },
|
||||
{ type: 'subheader', title: 'Invoervelden' },
|
||||
{ title: 'Tekstveld', value: 'text', props: { prependIcon: 'tabler-letter-t' } },
|
||||
{ title: 'Tekstvak (groot)', value: 'textarea', props: { prependIcon: 'tabler-text-size' } },
|
||||
{ title: 'Getal', value: 'number', props: { prependIcon: 'tabler-hash' } },
|
||||
{ title: 'Toggle (ja/nee)', value: 'boolean', props: { prependIcon: 'tabler-toggle-left' } },
|
||||
{ title: 'Dropdown', value: 'select', props: { prependIcon: 'tabler-list' } },
|
||||
{ title: 'Meervoudige keuze', value: 'multiselect', props: { prependIcon: 'tabler-checklist' } },
|
||||
{ title: 'Keuzerondjes', value: 'radio', props: { prependIcon: 'tabler-circle-dot' } },
|
||||
{ title: 'Checkbox', value: 'checkbox', props: { prependIcon: 'tabler-checkbox' } },
|
||||
{ title: 'Tags & Vaardigheden', value: 'tag_picker', props: { prependIcon: 'tabler-tags' } },
|
||||
]
|
||||
|
||||
const errors = ref<Record<string, string>>({})
|
||||
const refVForm = ref<VForm>()
|
||||
@@ -43,7 +57,6 @@ const defaultForm = () => ({
|
||||
is_filterable: false,
|
||||
is_portal_visible: true,
|
||||
is_admin_only: false,
|
||||
section: '',
|
||||
help_text: '',
|
||||
display_width: 'full' as FieldDisplayWidth,
|
||||
})
|
||||
@@ -54,6 +67,8 @@ const dialogTitle = computed(() =>
|
||||
props.field ? 'Veld bewerken' : 'Nieuw veld',
|
||||
)
|
||||
|
||||
const isHeading = computed(() => form.value.field_type === 'heading')
|
||||
|
||||
const showOptions = computed(() =>
|
||||
(FIELD_TYPES_WITH_OPTIONS as readonly string[]).includes(form.value.field_type),
|
||||
)
|
||||
@@ -75,7 +90,6 @@ watch(modelValue, (open) => {
|
||||
is_filterable: props.field.is_filterable,
|
||||
is_portal_visible: props.field.is_portal_visible,
|
||||
is_admin_only: props.field.is_admin_only,
|
||||
section: props.field.section ?? '',
|
||||
help_text: props.field.help_text ?? '',
|
||||
display_width: props.field.display_width ?? 'full',
|
||||
}
|
||||
@@ -101,11 +115,6 @@ function onSubmit() {
|
||||
errors.value = {}
|
||||
const payload: Partial<RegistrationFormFieldCreateDTO> = {
|
||||
label: form.value.label,
|
||||
is_required: form.value.is_required,
|
||||
is_filterable: form.value.is_filterable,
|
||||
is_portal_visible: form.value.is_portal_visible,
|
||||
is_admin_only: form.value.is_admin_only,
|
||||
section: form.value.section || null,
|
||||
help_text: form.value.help_text || null,
|
||||
display_width: form.value.display_width,
|
||||
}
|
||||
@@ -114,23 +123,30 @@ function onSubmit() {
|
||||
payload.field_type = form.value.field_type as RegistrationFieldType
|
||||
}
|
||||
|
||||
if (showOptions.value) {
|
||||
payload.options = form.value.options
|
||||
.filter(o => o.label.trim() !== '')
|
||||
.map(o => o.description.trim()
|
||||
? { label: o.label, description: o.description }
|
||||
: { label: o.label },
|
||||
)
|
||||
}
|
||||
else {
|
||||
payload.options = null
|
||||
}
|
||||
if (!isHeading.value) {
|
||||
payload.is_required = form.value.is_required
|
||||
payload.is_filterable = form.value.is_filterable
|
||||
payload.is_portal_visible = form.value.is_portal_visible
|
||||
payload.is_admin_only = form.value.is_admin_only
|
||||
|
||||
if (showTagCategory.value) {
|
||||
payload.tag_category = form.value.tag_category || null
|
||||
}
|
||||
else {
|
||||
payload.tag_category = null
|
||||
if (showOptions.value) {
|
||||
payload.options = form.value.options
|
||||
.filter(o => o.label.trim() !== '')
|
||||
.map(o => o.description.trim()
|
||||
? { label: o.label, description: o.description }
|
||||
: { label: o.label },
|
||||
)
|
||||
}
|
||||
else {
|
||||
payload.options = null
|
||||
}
|
||||
|
||||
if (showTagCategory.value) {
|
||||
payload.tag_category = form.value.tag_category || null
|
||||
}
|
||||
else {
|
||||
payload.tag_category = null
|
||||
}
|
||||
}
|
||||
|
||||
emit('save', payload)
|
||||
@@ -164,22 +180,11 @@ defineExpose({ setErrors })
|
||||
>
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<AppTextField
|
||||
v-model="form.label"
|
||||
label="Label"
|
||||
placeholder="Hoe heet dit veld?"
|
||||
:rules="[requiredValidator]"
|
||||
:error-messages="errors.label"
|
||||
autofocus
|
||||
autocomplete="one-time-code"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<AppSelect
|
||||
v-model="form.field_type"
|
||||
label="Type"
|
||||
:items="fieldTypeOptions"
|
||||
:items="fieldTypeItems"
|
||||
:rules="[requiredValidator]"
|
||||
:error-messages="errors.field_type"
|
||||
:disabled="!!field"
|
||||
@@ -188,170 +193,185 @@ defineExpose({ setErrors })
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- Options (conditional) -->
|
||||
<VCol
|
||||
v-if="showOptions"
|
||||
cols="12"
|
||||
>
|
||||
<label class="text-body-2 mb-2 d-block">Opties</label>
|
||||
<div
|
||||
v-for="(option, index) in form.options"
|
||||
:key="index"
|
||||
class="mb-3"
|
||||
>
|
||||
<VCard
|
||||
variant="outlined"
|
||||
class="pa-3 position-relative"
|
||||
<!-- HEADING fields: simplified UI -->
|
||||
<template v-if="isHeading">
|
||||
<VCol cols="12">
|
||||
<AppTextField
|
||||
v-model="form.label"
|
||||
label="Kop tekst"
|
||||
placeholder="bijv. Persoonlijke gegevens"
|
||||
:rules="[requiredValidator]"
|
||||
:error-messages="errors.label"
|
||||
autofocus
|
||||
autocomplete="one-time-code"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<AppTextField
|
||||
v-model="form.help_text"
|
||||
label="Omschrijving (optioneel)"
|
||||
placeholder="Korte toelichting onder de kop"
|
||||
:error-messages="errors.help_text"
|
||||
hint="Verschijnt in kleinere tekst onder de kop"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VAlert
|
||||
type="info"
|
||||
variant="tonal"
|
||||
density="compact"
|
||||
icon="tabler-info-circle"
|
||||
>
|
||||
<VRow dense>
|
||||
<VCol cols="12">
|
||||
<AppTextField
|
||||
v-model="option.label"
|
||||
:placeholder="`Optie ${index + 1}`"
|
||||
density="compact"
|
||||
hide-details
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<AppTextField
|
||||
v-model="option.description"
|
||||
label="Beschrijving (optioneel)"
|
||||
density="compact"
|
||||
placeholder="Korte toelichting die onder de optie verschijnt"
|
||||
hint="Max 200 tekens"
|
||||
counter="200"
|
||||
maxlength="200"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VBtn
|
||||
icon="tabler-x"
|
||||
variant="text"
|
||||
color="error"
|
||||
size="small"
|
||||
class="position-absolute"
|
||||
style="inset-block-start: 4px; inset-inline-end: 4px"
|
||||
@click="removeOption(index)"
|
||||
/>
|
||||
</VCard>
|
||||
</div>
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
size="small"
|
||||
prepend-icon="tabler-plus"
|
||||
@click="addOption"
|
||||
>
|
||||
Optie toevoegen
|
||||
</VBtn>
|
||||
<p
|
||||
v-if="errors.options"
|
||||
class="text-error text-caption mt-1"
|
||||
>
|
||||
{{ errors.options }}
|
||||
</p>
|
||||
</VCol>
|
||||
Een kop groepeert de velden hieronder visueel. Het verzamelt geen antwoorden.
|
||||
</VAlert>
|
||||
</VCol>
|
||||
</template>
|
||||
|
||||
<!-- Tag category (conditional) -->
|
||||
<VCol
|
||||
v-if="showTagCategory"
|
||||
cols="12"
|
||||
>
|
||||
<AppAutocomplete
|
||||
v-model="form.tag_category"
|
||||
label="Tag-categorie"
|
||||
:items="tagCategories ?? []"
|
||||
clearable
|
||||
:error-messages="errors.tag_category"
|
||||
placeholder="Alle tags (laat leeg voor alle categorieën)"
|
||||
/>
|
||||
</VCol>
|
||||
<!-- Regular input fields: full UI -->
|
||||
<template v-else>
|
||||
<VCol cols="12">
|
||||
<AppTextField
|
||||
v-model="form.label"
|
||||
label="Label"
|
||||
placeholder="Hoe heet dit veld?"
|
||||
:rules="[requiredValidator]"
|
||||
:error-messages="errors.label"
|
||||
autofocus
|
||||
autocomplete="one-time-code"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
v-model="form.section"
|
||||
label="Sectie"
|
||||
placeholder="bijv. Over jou"
|
||||
:error-messages="errors.section"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<label class="text-body-2 text-medium-emphasis d-block mb-1">
|
||||
Veldbreedte
|
||||
</label>
|
||||
<VBtnToggle
|
||||
v-model="form.display_width"
|
||||
mandatory
|
||||
density="compact"
|
||||
<!-- Options (conditional) -->
|
||||
<VCol
|
||||
v-if="showOptions"
|
||||
cols="12"
|
||||
>
|
||||
<label class="text-body-2 mb-2 d-block">Opties</label>
|
||||
<div
|
||||
v-for="(option, index) in form.options"
|
||||
:key="index"
|
||||
class="mb-3"
|
||||
>
|
||||
<VCard
|
||||
variant="outlined"
|
||||
class="pa-3 position-relative"
|
||||
>
|
||||
<VRow dense>
|
||||
<VCol cols="12">
|
||||
<AppTextField
|
||||
v-model="option.label"
|
||||
:placeholder="`Optie ${index + 1}`"
|
||||
density="compact"
|
||||
hide-details
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<AppTextField
|
||||
v-model="option.description"
|
||||
label="Beschrijving (optioneel)"
|
||||
density="compact"
|
||||
placeholder="Korte toelichting die onder de optie verschijnt"
|
||||
hint="Max 200 tekens"
|
||||
counter="200"
|
||||
maxlength="200"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VBtn
|
||||
icon="tabler-x"
|
||||
variant="text"
|
||||
color="error"
|
||||
size="small"
|
||||
class="position-absolute"
|
||||
style="inset-block-start: 4px; inset-inline-end: 4px"
|
||||
@click="removeOption(index)"
|
||||
/>
|
||||
</VCard>
|
||||
</div>
|
||||
<VBtn
|
||||
value="full"
|
||||
variant="tonal"
|
||||
size="small"
|
||||
prepend-icon="tabler-plus"
|
||||
@click="addOption"
|
||||
>
|
||||
<VIcon
|
||||
icon="tabler-layout-distribute-horizontal"
|
||||
size="16"
|
||||
class="me-1"
|
||||
/>
|
||||
Volledig
|
||||
Optie toevoegen
|
||||
</VBtn>
|
||||
<VBtn
|
||||
value="half"
|
||||
size="small"
|
||||
<p
|
||||
v-if="errors.options"
|
||||
class="text-error text-caption mt-1"
|
||||
>
|
||||
<VIcon
|
||||
icon="tabler-layout-columns"
|
||||
size="16"
|
||||
class="me-1"
|
||||
/>
|
||||
Half
|
||||
</VBtn>
|
||||
</VBtnToggle>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<AppTextarea
|
||||
v-model="form.help_text"
|
||||
label="Helptekst"
|
||||
placeholder="Toelichting die onder het veld wordt getoond"
|
||||
:error-messages="errors.help_text"
|
||||
rows="2"
|
||||
/>
|
||||
</VCol>
|
||||
{{ errors.options }}
|
||||
</p>
|
||||
</VCol>
|
||||
|
||||
<!-- Toggles -->
|
||||
<VCol cols="12">
|
||||
<div class="d-flex flex-wrap gap-x-6 gap-y-2">
|
||||
<VSwitch
|
||||
v-model="form.is_required"
|
||||
label="Verplicht"
|
||||
hide-details
|
||||
density="compact"
|
||||
<!-- Tag category (conditional) -->
|
||||
<VCol
|
||||
v-if="showTagCategory"
|
||||
cols="12"
|
||||
>
|
||||
<AppAutocomplete
|
||||
v-model="form.tag_category"
|
||||
label="Tag-categorie"
|
||||
:items="tagCategories ?? []"
|
||||
clearable
|
||||
:error-messages="errors.tag_category"
|
||||
placeholder="Alle tags (laat leeg voor alle categorieën)"
|
||||
/>
|
||||
<VSwitch
|
||||
v-model="form.is_filterable"
|
||||
label="Filterbaar bij inplannen"
|
||||
hide-details
|
||||
density="compact"
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<AppSelect
|
||||
v-model="form.display_width"
|
||||
:items="[
|
||||
{ title: 'Volledige breedte', value: 'full' },
|
||||
{ title: 'Halve breedte', value: 'half' },
|
||||
]"
|
||||
label="Veldbreedte"
|
||||
:hint="form.display_width === 'half' ? 'Wordt naast een ander half-breed veld geplaatst' : ''"
|
||||
persistent-hint
|
||||
/>
|
||||
<VSwitch
|
||||
v-model="form.is_portal_visible"
|
||||
label="Zichtbaar voor deelnemer"
|
||||
hide-details
|
||||
density="compact"
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<AppTextarea
|
||||
v-model="form.help_text"
|
||||
label="Helptekst"
|
||||
placeholder="Toelichting die onder het veld wordt getoond"
|
||||
:error-messages="errors.help_text"
|
||||
rows="2"
|
||||
/>
|
||||
<VSwitch
|
||||
v-model="form.is_admin_only"
|
||||
label="Alleen voor organisator"
|
||||
hide-details
|
||||
density="compact"
|
||||
/>
|
||||
</div>
|
||||
</VCol>
|
||||
</VCol>
|
||||
|
||||
<!-- Toggles -->
|
||||
<VCol cols="12">
|
||||
<div class="d-flex flex-wrap gap-x-6 gap-y-2">
|
||||
<VSwitch
|
||||
v-model="form.is_required"
|
||||
label="Verplicht"
|
||||
hide-details
|
||||
density="compact"
|
||||
/>
|
||||
<VSwitch
|
||||
v-model="form.is_filterable"
|
||||
label="Filterbaar bij inplannen"
|
||||
hide-details
|
||||
density="compact"
|
||||
/>
|
||||
<VSwitch
|
||||
v-model="form.is_portal_visible"
|
||||
label="Zichtbaar voor deelnemer"
|
||||
hide-details
|
||||
density="compact"
|
||||
/>
|
||||
<VSwitch
|
||||
v-model="form.is_admin_only"
|
||||
label="Alleen voor organisator"
|
||||
hide-details
|
||||
density="compact"
|
||||
/>
|
||||
</div>
|
||||
</VCol>
|
||||
</template>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
|
||||
@@ -88,9 +88,6 @@ function onAdd(templateId: string) {
|
||||
</VListItemTitle>
|
||||
<VListItemSubtitle>
|
||||
{{ FIELD_TYPE_LABELS[template.field_type] ?? template.field_type }}
|
||||
<template v-if="template.section">
|
||||
· {{ template.section }}
|
||||
</template>
|
||||
<template v-if="template.is_required">
|
||||
· Verplicht
|
||||
</template>
|
||||
|
||||
@@ -10,6 +10,22 @@ import { usePersonTagCategories } from '@/composables/api/usePersonTags'
|
||||
import { requiredValidator } from '@core/utils/validators'
|
||||
import type { RegistrationFieldTemplate, RegistrationFieldTemplateCreateDTO, RegistrationFieldType, FieldDisplayWidth } from '@/types/registration-field-template'
|
||||
import { FIELD_TYPE_LABELS, FIELD_TYPES_WITH_OPTIONS } from '@/types/registration-field-template'
|
||||
|
||||
const fieldTypeItems = [
|
||||
{ type: 'subheader', title: 'Structuur' },
|
||||
{ title: 'Kop / Sectie-titel', value: 'heading', props: { prependIcon: 'tabler-heading' } },
|
||||
{ type: 'divider' },
|
||||
{ type: 'subheader', title: 'Invoervelden' },
|
||||
{ title: 'Tekstveld', value: 'text', props: { prependIcon: 'tabler-letter-t' } },
|
||||
{ title: 'Tekstvak (groot)', value: 'textarea', props: { prependIcon: 'tabler-text-size' } },
|
||||
{ title: 'Getal', value: 'number', props: { prependIcon: 'tabler-hash' } },
|
||||
{ title: 'Toggle (ja/nee)', value: 'boolean', props: { prependIcon: 'tabler-toggle-left' } },
|
||||
{ title: 'Dropdown', value: 'select', props: { prependIcon: 'tabler-list' } },
|
||||
{ title: 'Meervoudige keuze', value: 'multiselect', props: { prependIcon: 'tabler-checklist' } },
|
||||
{ title: 'Keuzerondjes', value: 'radio', props: { prependIcon: 'tabler-circle-dot' } },
|
||||
{ title: 'Checkbox', value: 'checkbox', props: { prependIcon: 'tabler-checkbox' } },
|
||||
{ title: 'Tags & Vaardigheden', value: 'tag_picker', props: { prependIcon: 'tabler-tags' } },
|
||||
]
|
||||
import type { AxiosError } from 'axios'
|
||||
import type { ApiErrorResponse } from '@/types/auth'
|
||||
|
||||
@@ -25,15 +41,12 @@ const { mutate: createTemplate, isPending: isCreating } = useCreateRegistrationF
|
||||
const { mutate: updateTemplate, isPending: isUpdating } = useUpdateRegistrationFieldTemplate(orgIdRef)
|
||||
const { mutate: deleteTemplate, isPending: isDeleting } = useDeleteRegistrationFieldTemplate(orgIdRef)
|
||||
|
||||
const fieldTypeOptions = Object.entries(FIELD_TYPE_LABELS).map(([value, title]) => ({ title, value }))
|
||||
|
||||
const activeTemplates = computed(() => templates.value?.filter(t => t.is_active) ?? [])
|
||||
const inactiveTemplates = computed(() => templates.value?.filter(t => !t.is_active) ?? [])
|
||||
|
||||
const headers = [
|
||||
{ title: 'Label', key: 'label' },
|
||||
{ title: 'Type', key: 'field_type' },
|
||||
{ title: 'Sectie', key: 'section' },
|
||||
{ title: 'Systeem', key: 'is_system', sortable: false },
|
||||
{ title: 'Acties', key: 'actions', sortable: false, align: 'end' as const },
|
||||
]
|
||||
@@ -60,7 +73,6 @@ const defaultForm = () => ({
|
||||
is_filterable: false,
|
||||
is_portal_visible: true,
|
||||
is_admin_only: false,
|
||||
section: '',
|
||||
help_text: '',
|
||||
sort_order: 0,
|
||||
display_width: 'full' as FieldDisplayWidth,
|
||||
@@ -74,6 +86,8 @@ const dialogTitle = computed(() =>
|
||||
|
||||
const isSaving = computed(() => isCreating.value || isUpdating.value)
|
||||
|
||||
const isHeading = computed(() => form.value.field_type === 'heading')
|
||||
|
||||
const showOptions = computed(() =>
|
||||
(FIELD_TYPES_WITH_OPTIONS as readonly string[]).includes(form.value.field_type),
|
||||
)
|
||||
@@ -104,7 +118,6 @@ function openEditDialog(template: RegistrationFieldTemplate) {
|
||||
is_filterable: template.is_filterable,
|
||||
is_portal_visible: template.is_portal_visible,
|
||||
is_admin_only: template.is_admin_only,
|
||||
section: template.section ?? '',
|
||||
help_text: template.help_text ?? '',
|
||||
sort_order: template.sort_order,
|
||||
display_width: template.display_width ?? 'full',
|
||||
@@ -128,11 +141,6 @@ function onSubmit() {
|
||||
errors.value = {}
|
||||
const payload: Partial<RegistrationFieldTemplateCreateDTO> = {
|
||||
label: form.value.label,
|
||||
is_required: form.value.is_required,
|
||||
is_filterable: form.value.is_filterable,
|
||||
is_portal_visible: form.value.is_portal_visible,
|
||||
is_admin_only: form.value.is_admin_only,
|
||||
section: form.value.section || null,
|
||||
help_text: form.value.help_text || null,
|
||||
sort_order: form.value.sort_order,
|
||||
display_width: form.value.display_width,
|
||||
@@ -142,20 +150,27 @@ function onSubmit() {
|
||||
payload.field_type = form.value.field_type as RegistrationFieldType
|
||||
}
|
||||
|
||||
if (showOptions.value) {
|
||||
payload.options = form.value.options
|
||||
.filter(o => o.label.trim() !== '')
|
||||
.map(o => o.description.trim()
|
||||
? { label: o.label, description: o.description }
|
||||
: { label: o.label },
|
||||
)
|
||||
}
|
||||
else {
|
||||
payload.options = null
|
||||
}
|
||||
if (!isHeading.value) {
|
||||
payload.is_required = form.value.is_required
|
||||
payload.is_filterable = form.value.is_filterable
|
||||
payload.is_portal_visible = form.value.is_portal_visible
|
||||
payload.is_admin_only = form.value.is_admin_only
|
||||
|
||||
if (showTagCategory.value) {
|
||||
payload.tag_category = form.value.tag_category || null
|
||||
if (showOptions.value) {
|
||||
payload.options = form.value.options
|
||||
.filter(o => o.label.trim() !== '')
|
||||
.map(o => o.description.trim()
|
||||
? { label: o.label, description: o.description }
|
||||
: { label: o.label },
|
||||
)
|
||||
}
|
||||
else {
|
||||
payload.options = null
|
||||
}
|
||||
|
||||
if (showTagCategory.value) {
|
||||
payload.tag_category = form.value.tag_category || null
|
||||
}
|
||||
}
|
||||
|
||||
if (editingTemplate.value) {
|
||||
@@ -301,14 +316,6 @@ function activate(template: RegistrationFieldTemplate) {
|
||||
{{ FIELD_TYPE_LABELS[item.field_type] ?? item.field_type }}
|
||||
</template>
|
||||
|
||||
<template #item.section="{ item }">
|
||||
<span v-if="item.section">{{ item.section }}</span>
|
||||
<span
|
||||
v-else
|
||||
class="text-disabled"
|
||||
>-</span>
|
||||
</template>
|
||||
|
||||
<template #item.is_system="{ item }">
|
||||
<VChip
|
||||
v-if="item.is_system"
|
||||
@@ -398,22 +405,11 @@ function activate(template: RegistrationFieldTemplate) {
|
||||
>
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<AppTextField
|
||||
v-model="form.label"
|
||||
label="Label"
|
||||
placeholder="Hoe heet dit veld?"
|
||||
:rules="[requiredValidator]"
|
||||
:error-messages="errors.label"
|
||||
autofocus
|
||||
autocomplete="one-time-code"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<AppSelect
|
||||
v-model="form.field_type"
|
||||
label="Type"
|
||||
:items="fieldTypeOptions"
|
||||
:items="fieldTypeItems"
|
||||
:rules="[requiredValidator]"
|
||||
:error-messages="errors.field_type"
|
||||
:disabled="!!editingTemplate"
|
||||
@@ -422,181 +418,199 @@ function activate(template: RegistrationFieldTemplate) {
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- Options (conditional) -->
|
||||
<VCol
|
||||
v-if="showOptions"
|
||||
cols="12"
|
||||
>
|
||||
<label class="text-body-2 mb-2 d-block">Opties</label>
|
||||
<div
|
||||
v-for="(option, index) in form.options"
|
||||
:key="index"
|
||||
class="mb-3"
|
||||
>
|
||||
<VCard
|
||||
variant="outlined"
|
||||
class="pa-3 position-relative"
|
||||
<!-- HEADING fields: simplified UI -->
|
||||
<template v-if="isHeading">
|
||||
<VCol cols="12">
|
||||
<AppTextField
|
||||
v-model="form.label"
|
||||
label="Kop tekst"
|
||||
placeholder="bijv. Persoonlijke gegevens"
|
||||
:rules="[requiredValidator]"
|
||||
:error-messages="errors.label"
|
||||
autofocus
|
||||
autocomplete="one-time-code"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<AppTextField
|
||||
v-model="form.help_text"
|
||||
label="Omschrijving (optioneel)"
|
||||
placeholder="Korte toelichting onder de kop"
|
||||
:error-messages="errors.help_text"
|
||||
hint="Verschijnt in kleinere tekst onder de kop"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VAlert
|
||||
type="info"
|
||||
variant="tonal"
|
||||
density="compact"
|
||||
icon="tabler-info-circle"
|
||||
>
|
||||
<VRow dense>
|
||||
<VCol cols="12">
|
||||
<AppTextField
|
||||
v-model="option.label"
|
||||
:placeholder="`Optie ${index + 1}`"
|
||||
density="compact"
|
||||
hide-details
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<AppTextField
|
||||
v-model="option.description"
|
||||
label="Beschrijving (optioneel)"
|
||||
density="compact"
|
||||
placeholder="Korte toelichting die onder de optie verschijnt"
|
||||
hint="Max 200 tekens"
|
||||
counter="200"
|
||||
maxlength="200"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VBtn
|
||||
icon="tabler-x"
|
||||
variant="text"
|
||||
color="error"
|
||||
size="small"
|
||||
class="position-absolute"
|
||||
style="inset-block-start: 4px; inset-inline-end: 4px"
|
||||
@click="removeOption(index)"
|
||||
/>
|
||||
</VCard>
|
||||
</div>
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
size="small"
|
||||
prepend-icon="tabler-plus"
|
||||
@click="addOption"
|
||||
>
|
||||
Optie toevoegen
|
||||
</VBtn>
|
||||
<p
|
||||
v-if="errors.options"
|
||||
class="text-error text-caption mt-1"
|
||||
>
|
||||
{{ errors.options }}
|
||||
</p>
|
||||
</VCol>
|
||||
Een kop groepeert de velden hieronder visueel. Het verzamelt geen antwoorden.
|
||||
</VAlert>
|
||||
</VCol>
|
||||
</template>
|
||||
|
||||
<!-- Tag category (conditional) -->
|
||||
<VCol
|
||||
v-if="showTagCategory"
|
||||
cols="12"
|
||||
>
|
||||
<AppAutocomplete
|
||||
v-model="form.tag_category"
|
||||
label="Tag-categorie"
|
||||
:items="tagCategories ?? []"
|
||||
clearable
|
||||
:error-messages="errors.tag_category"
|
||||
placeholder="Alle tags (laat leeg voor alle categorieën)"
|
||||
/>
|
||||
</VCol>
|
||||
<!-- Regular input fields: full UI -->
|
||||
<template v-else>
|
||||
<VCol cols="12">
|
||||
<AppTextField
|
||||
v-model="form.label"
|
||||
label="Label"
|
||||
placeholder="Hoe heet dit veld?"
|
||||
:rules="[requiredValidator]"
|
||||
:error-messages="errors.label"
|
||||
autofocus
|
||||
autocomplete="one-time-code"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
v-model="form.section"
|
||||
label="Sectie"
|
||||
placeholder="bijv. Vergoeding"
|
||||
:error-messages="errors.section"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
v-model.number="form.sort_order"
|
||||
label="Volgorde"
|
||||
type="number"
|
||||
:error-messages="errors.sort_order"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<label class="text-body-2 text-medium-emphasis d-block mb-1">
|
||||
Veldbreedte
|
||||
</label>
|
||||
<VBtnToggle
|
||||
v-model="form.display_width"
|
||||
mandatory
|
||||
density="compact"
|
||||
<!-- Options (conditional) -->
|
||||
<VCol
|
||||
v-if="showOptions"
|
||||
cols="12"
|
||||
>
|
||||
<label class="text-body-2 mb-2 d-block">Opties</label>
|
||||
<div
|
||||
v-for="(option, index) in form.options"
|
||||
:key="index"
|
||||
class="mb-3"
|
||||
>
|
||||
<VCard
|
||||
variant="outlined"
|
||||
class="pa-3 position-relative"
|
||||
>
|
||||
<VRow dense>
|
||||
<VCol cols="12">
|
||||
<AppTextField
|
||||
v-model="option.label"
|
||||
:placeholder="`Optie ${index + 1}`"
|
||||
density="compact"
|
||||
hide-details
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<AppTextField
|
||||
v-model="option.description"
|
||||
label="Beschrijving (optioneel)"
|
||||
density="compact"
|
||||
placeholder="Korte toelichting die onder de optie verschijnt"
|
||||
hint="Max 200 tekens"
|
||||
counter="200"
|
||||
maxlength="200"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VBtn
|
||||
icon="tabler-x"
|
||||
variant="text"
|
||||
color="error"
|
||||
size="small"
|
||||
class="position-absolute"
|
||||
style="inset-block-start: 4px; inset-inline-end: 4px"
|
||||
@click="removeOption(index)"
|
||||
/>
|
||||
</VCard>
|
||||
</div>
|
||||
<VBtn
|
||||
value="full"
|
||||
variant="tonal"
|
||||
size="small"
|
||||
prepend-icon="tabler-plus"
|
||||
@click="addOption"
|
||||
>
|
||||
<VIcon
|
||||
icon="tabler-layout-distribute-horizontal"
|
||||
size="16"
|
||||
class="me-1"
|
||||
/>
|
||||
Volledig
|
||||
Optie toevoegen
|
||||
</VBtn>
|
||||
<VBtn
|
||||
value="half"
|
||||
size="small"
|
||||
<p
|
||||
v-if="errors.options"
|
||||
class="text-error text-caption mt-1"
|
||||
>
|
||||
<VIcon
|
||||
icon="tabler-layout-columns"
|
||||
size="16"
|
||||
class="me-1"
|
||||
/>
|
||||
Half
|
||||
</VBtn>
|
||||
</VBtnToggle>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<AppTextarea
|
||||
v-model="form.help_text"
|
||||
label="Helptekst"
|
||||
placeholder="Toelichting die onder het veld wordt getoond"
|
||||
:error-messages="errors.help_text"
|
||||
rows="2"
|
||||
/>
|
||||
</VCol>
|
||||
{{ errors.options }}
|
||||
</p>
|
||||
</VCol>
|
||||
|
||||
<!-- Toggles -->
|
||||
<VCol cols="12">
|
||||
<div class="d-flex flex-wrap gap-x-6 gap-y-2">
|
||||
<VSwitch
|
||||
v-model="form.is_required"
|
||||
label="Verplicht"
|
||||
hide-details
|
||||
density="compact"
|
||||
<!-- Tag category (conditional) -->
|
||||
<VCol
|
||||
v-if="showTagCategory"
|
||||
cols="12"
|
||||
>
|
||||
<AppAutocomplete
|
||||
v-model="form.tag_category"
|
||||
label="Tag-categorie"
|
||||
:items="tagCategories ?? []"
|
||||
clearable
|
||||
:error-messages="errors.tag_category"
|
||||
placeholder="Alle tags (laat leeg voor alle categorieën)"
|
||||
/>
|
||||
<VSwitch
|
||||
v-model="form.is_filterable"
|
||||
label="Filterbaar bij inplannen"
|
||||
hide-details
|
||||
density="compact"
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppSelect
|
||||
v-model="form.display_width"
|
||||
:items="[
|
||||
{ title: 'Volledige breedte', value: 'full' },
|
||||
{ title: 'Halve breedte', value: 'half' },
|
||||
]"
|
||||
label="Veldbreedte"
|
||||
:hint="form.display_width === 'half' ? 'Wordt naast een ander half-breed veld geplaatst' : ''"
|
||||
persistent-hint
|
||||
/>
|
||||
<VSwitch
|
||||
v-model="form.is_portal_visible"
|
||||
label="Zichtbaar voor deelnemer"
|
||||
hide-details
|
||||
density="compact"
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
v-model.number="form.sort_order"
|
||||
label="Volgorde"
|
||||
type="number"
|
||||
:error-messages="errors.sort_order"
|
||||
/>
|
||||
<VSwitch
|
||||
v-model="form.is_admin_only"
|
||||
label="Alleen voor organisator"
|
||||
hide-details
|
||||
density="compact"
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<AppTextarea
|
||||
v-model="form.help_text"
|
||||
label="Helptekst"
|
||||
placeholder="Toelichting die onder het veld wordt getoond"
|
||||
:error-messages="errors.help_text"
|
||||
rows="2"
|
||||
/>
|
||||
</div>
|
||||
</VCol>
|
||||
</VCol>
|
||||
|
||||
<!-- Toggles -->
|
||||
<VCol cols="12">
|
||||
<div class="d-flex flex-wrap gap-x-6 gap-y-2">
|
||||
<VSwitch
|
||||
v-model="form.is_required"
|
||||
label="Verplicht"
|
||||
hide-details
|
||||
density="compact"
|
||||
/>
|
||||
<VSwitch
|
||||
v-model="form.is_filterable"
|
||||
label="Filterbaar bij inplannen"
|
||||
hide-details
|
||||
density="compact"
|
||||
/>
|
||||
<VSwitch
|
||||
v-model="form.is_portal_visible"
|
||||
label="Zichtbaar voor deelnemer"
|
||||
hide-details
|
||||
density="compact"
|
||||
/>
|
||||
<VSwitch
|
||||
v-model="form.is_admin_only"
|
||||
label="Alleen voor organisator"
|
||||
hide-details
|
||||
density="compact"
|
||||
/>
|
||||
</div>
|
||||
</VCol>
|
||||
</template>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
|
||||
@@ -8,6 +8,7 @@ export const RegistrationFieldType = {
|
||||
BOOLEAN: 'boolean',
|
||||
NUMBER: 'number',
|
||||
TAG_PICKER: 'tag_picker',
|
||||
HEADING: 'heading',
|
||||
} as const
|
||||
|
||||
export type RegistrationFieldType = typeof RegistrationFieldType[keyof typeof RegistrationFieldType]
|
||||
@@ -22,6 +23,7 @@ export const FIELD_TYPE_LABELS: Record<RegistrationFieldType, string> = {
|
||||
boolean: 'Ja/Nee',
|
||||
number: 'Getal',
|
||||
tag_picker: 'Tag-kiezer',
|
||||
heading: 'Kop / Sectie-titel',
|
||||
}
|
||||
|
||||
export const FIELD_TYPES_WITH_OPTIONS: RegistrationFieldType[] = [
|
||||
@@ -52,7 +54,6 @@ export interface RegistrationFieldTemplate {
|
||||
is_filterable: boolean
|
||||
is_portal_visible: boolean
|
||||
is_admin_only: boolean
|
||||
section: string | null
|
||||
help_text: string | null
|
||||
sort_order: number
|
||||
display_width: FieldDisplayWidth
|
||||
@@ -69,7 +70,6 @@ export interface RegistrationFieldTemplateCreateDTO {
|
||||
is_filterable?: boolean
|
||||
is_portal_visible?: boolean
|
||||
is_admin_only?: boolean
|
||||
section?: string | null
|
||||
help_text?: string | null
|
||||
sort_order?: number
|
||||
display_width?: FieldDisplayWidth
|
||||
|
||||
@@ -22,7 +22,6 @@ export interface RegistrationFormField {
|
||||
is_portal_visible: boolean
|
||||
is_admin_only: boolean
|
||||
is_filterable: boolean
|
||||
section: string | null
|
||||
help_text: string | null
|
||||
sort_order: number
|
||||
display_width: FieldDisplayWidth
|
||||
@@ -40,7 +39,6 @@ export interface RegistrationFormFieldCreateDTO {
|
||||
is_portal_visible?: boolean
|
||||
is_admin_only?: boolean
|
||||
is_filterable?: boolean
|
||||
section?: string | null
|
||||
help_text?: string | null
|
||||
sort_order?: number
|
||||
display_width?: FieldDisplayWidth
|
||||
|
||||
Reference in New Issue
Block a user