Files
crewli/apps/app/src/components/event/RegistrationFieldFormDialog.vue
bert.hausmans 47bd533179 style(app): apply eslint --fix to Tier 1 (Vue templates)
WS-3 session 1b-i Tier 1.

Scope: src/components/**, src/pages/**, src/layouts/**, src/views/**
restricted to *.vue files. Mechanical formatting only — predominantly
vue/html-indent (506 fixes in CrowdListDetailPanel.vue alone),
padding-line-between-statements, antfu/if-newline.

Excludes (per session prompt):
- apps/app/vite.config.ts (Tier 3)
- apps/app/themeConfig.ts (Tier 3)
- apps/app/vitest.config.ts (Tier 3)
- All TypeScript-only files in src/composables, src/lib, src/stores,
  src/plugins, src/types (Tier 2 — separate commit)

Includes session 1a layouts (PortalLayout.vue, PublicLayout.vue) where
2 'lines-around-comment' errors were flagged in the previous 1b-i
pre-flight inspection.

Tests + typecheck verified green post-fix:
- apps/app vitest: 49 passed (unchanged)
- apps/app vue-tsc: clean (unchanged)
- apps/portal vitest: 113 passed (unchanged — not touched)
- backend pest: 1486 passed (unchanged — not touched)

Lint baseline progression:
- Pre-Tier-1: 1451 problems
- Post-Tier-1: 422 problems

Visual smoke status:
- NOT YET SMOKED — Bert to verify before merge. This Claude Code
  session has no UI access; cannot run pnpm dev and click through
  affected routes. The high-traffic candidates are
  CrowdListDetailPanel (506 fixes), AssignPersonDialog (44),
  ShiftDetailPanel (36), and the events / form-failures pages.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 11:04:46 +02:00

425 lines
14 KiB
Vue

<script setup lang="ts">
import { VForm } from 'vuetify/components/VForm'
import type { AxiosError } from 'axios'
import { usePersonTagCategories } from '@/composables/api/usePersonTags'
import { requiredValidator } from '@core/utils/validators'
import type { RegistrationFormField, RegistrationFormFieldCreateDTO } from '@/types/registration-form-field'
import type { FieldDisplayWidth, RegistrationFieldType } from '@/types/registration-field-template'
import { FIELD_TYPES_WITH_OPTIONS } from '@/types/registration-field-template'
import type { ApiErrorResponse } from '@/types/auth'
const props = defineProps<{
orgId: string
field?: RegistrationFormField | null
isSaving: boolean
}>()
const emit = defineEmits<{
save: [payload: Partial<RegistrationFormFieldCreateDTO>]
'update:modelValue': [value: boolean]
}>()
const modelValue = defineModel<boolean>({ default: false })
const orgIdRef = computed(() => props.orgId)
const { data: tagCategories } = usePersonTagCategories(orgIdRef)
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>()
interface OptionEntry {
label: string
description: string
}
const defaultForm = () => ({
label: '',
field_type: 'text' as string,
options: [] as OptionEntry[],
tag_categories: [] as string[],
is_required: false,
is_filterable: false,
is_portal_visible: true,
is_admin_only: false,
help_text: '',
display_width: 'full' as FieldDisplayWidth,
})
const form = ref(defaultForm())
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),
)
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.normalized_options
? props.field.normalized_options.map(o => ({ label: o.label, description: o.description ?? '' }))
: [],
tag_categories: props.field.tag_categories ?? [],
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,
help_text: props.field.help_text ?? '',
display_width: props.field.display_width ?? 'full',
}
}
else {
form.value = defaultForm()
}
}
})
function addOption() {
form.value.options.push({ label: '', description: '' })
}
function removeOption(index: number) {
form.value.options.splice(index, 1)
}
function onSubmit() {
refVForm.value?.validate().then(({ valid }) => {
if (!valid)
return
errors.value = {}
const payload: Partial<RegistrationFormFieldCreateDTO> = {
label: form.value.label,
help_text: form.value.help_text || null,
display_width: form.value.display_width,
}
if (!props.field)
payload.field_type = form.value.field_type as RegistrationFieldType
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 (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_categories = form.value.tag_categories.length > 0 ? form.value.tag_categories : null
else
payload.tag_categories = null
}
emit('save', payload)
})
}
function setErrors(err: Error) {
const data = (err as AxiosError<ApiErrorResponse>).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">
<AppSelect
v-model="form.field_type"
label="Type"
:items="fieldTypeItems"
:rules="[requiredValidator]"
:error-messages="errors.field_type"
:disabled="!!field"
:hint="field ? 'Type kan niet gewijzigd worden na aanmaken' : ''"
:persistent-hint="!!field"
/>
</VCol>
<!-- 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"
>
Een kop groepeert de velden hieronder visueel. Het verzamelt geen antwoorden.
</VAlert>
</VCol>
</template>
<!-- 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">
<AppTextarea
v-model="form.help_text"
label="Helptekst"
placeholder="Toelichting die onder het veld wordt getoond"
:error-messages="errors.help_text"
rows="2"
/>
</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
/>
</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>
<!-- Options section at bottom with visual separation -->
<template v-if="showOptions">
<VCol cols="12">
<VDivider class="mb-1" />
</VCol>
<VCol cols="12">
<div class="d-flex align-center justify-space-between mb-3">
<div>
<div class="text-subtitle-2">
Opties
</div>
<div class="text-caption text-medium-emphasis">
Waaruit kan de deelnemer kiezen?
</div>
</div>
<VBtn
variant="tonal"
size="small"
prepend-icon="tabler-plus"
@click="addOption"
>
Optie toevoegen
</VBtn>
</div>
<div
v-for="(option, index) in form.options"
:key="index"
class="mb-2"
>
<div class="d-flex align-center gap-2">
<VIcon
icon="tabler-grip-vertical"
size="16"
class="text-medium-emphasis flex-shrink-0"
style="cursor: grab;"
/>
<AppTextField
v-model="option.label"
placeholder="Optie"
density="compact"
hide-details
class="flex-grow-1"
style="max-inline-size: 200px;"
/>
<AppTextField
v-model="option.description"
placeholder="Beschrijving (optioneel)"
density="compact"
hide-details
class="flex-grow-1"
/>
<VBtn
icon="tabler-trash"
variant="text"
color="error"
size="x-small"
class="flex-shrink-0"
@click="removeOption(index)"
/>
</div>
</div>
<div
v-if="form.options.length === 0"
class="text-center text-medium-emphasis py-4"
>
Nog geen opties. Klik op "Optie toevoegen" om te beginnen.
</div>
<p
v-if="errors.options"
class="text-error text-caption mt-1"
>
{{ errors.options }}
</p>
</VCol>
</template>
<!-- Tag categories (conditional) -->
<template v-if="showTagCategory">
<VCol cols="12">
<VDivider class="mb-1" />
</VCol>
<VCol cols="12">
<div class="text-subtitle-2 mb-1">
Tag-categorieën
</div>
<div class="text-caption text-medium-emphasis mb-3">
Kies één of meerdere categorieën. De deelnemer ziet alle tags uit deze categorieën.
</div>
<AppAutocomplete
v-model="form.tag_categories"
label="Categorieën"
:items="tagCategories ?? []"
multiple
chips
closable-chips
clearable
:error-messages="errors.tag_categories"
placeholder="Alle tags (laat leeg voor alle categorieën)"
/>
</VCol>
</template>
</template>
</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>