Files
crewli/apps/app/src/components/event/RegistrationFieldFormDialog.vue
bert.hausmans a9dcee0fc7 feat(app): registration fields management page in event settings
Adds a new settings sub-page for managing dynamic registration form fields
per event. Includes sortable field list, create/edit dialog, template picker,
and import-from-event functionality.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 23:44:14 +02:00

305 lines
8.3 KiB
Vue

<script setup lang="ts">
import { VForm } from 'vuetify/components/VForm'
import { usePersonTagCategories } from '@/composables/api/usePersonTags'
import { requiredValidator } from '@core/utils/validators'
import type { RegistrationFormField } from '@/types/registration-form-field'
import { FIELD_TYPE_LABELS, FIELD_TYPES_WITH_OPTIONS } from '@/types/registration-field-template'
const props = defineProps<{
orgId: string
field?: RegistrationFormField | null
isSaving: boolean
}>()
const emit = defineEmits<{
save: [payload: Record<string, any>]
'update:modelValue': [value: boolean]
}>()
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 errors = ref<Record<string, string>>({})
const refVForm = ref<VForm>()
const defaultForm = () => ({
label: '',
field_type: 'text' as string,
options: [] as string[],
tag_category: null as string | null,
is_required: false,
is_filterable: false,
is_portal_visible: true,
is_admin_only: false,
section: '',
help_text: '',
})
const form = ref(defaultForm())
const dialogTitle = computed(() =>
props.field ? 'Veld bewerken' : 'Nieuw veld',
)
const showOptions = computed(() =>
FIELD_TYPES_WITH_OPTIONS.includes(form.value.field_type as any),
)
const showTagCategory = computed(() => form.value.field_type === 'tag_picker')
watch(modelValue, (open) => {
if (open) {
errors.value = {}
if (props.field) {
form.value = {
label: props.field.label,
field_type: props.field.field_type,
options: props.field.options ? [...props.field.options] : [],
tag_category: props.field.tag_category,
is_required: props.field.is_required,
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 ?? '',
}
}
else {
form.value = defaultForm()
}
}
})
function addOption() {
form.value.options.push('')
}
function removeOption(index: number) {
form.value.options.splice(index, 1)
}
function onSubmit() {
refVForm.value?.validate().then(({ valid }) => {
if (!valid) return
errors.value = {}
const payload: Record<string, any> = {
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,
}
if (!props.field) {
payload.field_type = form.value.field_type
}
if (showOptions.value) {
payload.options = form.value.options.filter(o => o.trim() !== '')
}
else {
payload.options = null
}
if (showTagCategory.value) {
payload.tag_category = form.value.tag_category || null
}
else {
payload.tag_category = null
}
emit('save', payload)
})
}
function setErrors(err: any) {
const data = err.response?.data
if (data?.errors) {
errors.value = Object.fromEntries(
Object.entries(data.errors).map(([k, v]) => [k, (v as string[])[0]]),
)
}
else if (data?.message) {
errors.value = { label: data.message }
}
}
defineExpose({ setErrors })
</script>
<template>
<VDialog
v-model="modelValue"
max-width="650"
>
<VCard :title="dialogTitle">
<VForm
ref="refVForm"
@submit.prevent="onSubmit"
>
<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"
:rules="[requiredValidator]"
:error-messages="errors.field_type"
:disabled="!!field"
:hint="field ? 'Type kan niet gewijzigd worden na aanmaken' : ''"
:persistent-hint="!!field"
/>
</VCol>
<!-- Options (conditional) -->
<VCol
v-if="showOptions"
cols="12"
>
<label class="text-body-2 mb-2 d-block">Opties</label>
<div
v-for="(_, index) in form.options"
:key="index"
class="d-flex align-center gap-x-2 mb-2"
>
<AppTextField
v-model="form.options[index]"
:placeholder="`Optie ${index + 1}`"
density="compact"
hide-details
/>
<VBtn
icon="tabler-x"
variant="text"
size="small"
color="error"
@click="removeOption(index)"
/>
</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>
<!-- 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>
<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"
/>
<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>
<!-- 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>
</VRow>
</VCardText>
<VCardActions>
<VSpacer />
<VBtn
variant="text"
@click="modelValue = false"
>
Annuleren
</VBtn>
<VBtn
type="submit"
color="primary"
:loading="isSaving"
>
Opslaan
</VBtn>
</VCardActions>
</VForm>
</VCard>
</VDialog>
</template>