feat(organisation): rebuild EditOrganisationDialog with contact fields

Vervang het naam-alleen dialoog door een volledig organisatiegegevens-
formulier: naam, slug (met copy-knop en tooltip), contactpersoon, contact
e-mail, telefoon en website. Slug krijgt een regex-validator; e-mail en
URL alleen gevalideerd wanneer ingevuld. Server-side validatiefouten per
veld getoond.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-17 10:28:04 +02:00
parent 027c5dac4e
commit d4d719a667

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { VForm } from 'vuetify/components/VForm'
import { useUpdateOrganisation } from '@/composables/api/useOrganisations'
import { requiredValidator } from '@core/utils/validators'
import { emailValidator, requiredValidator, urlValidator } from '@core/utils/validators'
import type { Organisation } from '@/types/organisation'
import type { AxiosError } from 'axios'
import type { ApiErrorResponse } from '@/types/auth'
@@ -12,16 +12,52 @@ const props = defineProps<{
const modelValue = defineModel<boolean>({ required: true })
const name = ref('')
interface FormState {
name: string
slug: string
contact_name: string
contact_email: string
phone: string
website: string
}
const initialState = (): FormState => ({
name: props.organisation.name ?? '',
slug: props.organisation.slug ?? '',
contact_name: props.organisation.contact_name ?? '',
contact_email: props.organisation.contact_email ?? '',
phone: props.organisation.phone ?? '',
website: props.organisation.website ?? '',
})
const form = ref<FormState>(initialState())
const errors = ref<Record<string, string>>({})
const refVForm = ref<VForm>()
const showSuccess = ref(false)
const showCopied = ref(false)
const { mutate: updateOrganisation, isPending } = useUpdateOrganisation()
watch(() => props.organisation, (org) => {
name.value = org.name
}, { immediate: true })
const slugRegexValidator = (value: string) =>
/^[a-z0-9-]+$/.test(value) || 'Alleen kleine letters, cijfers en streepjes toegestaan.'
watch(
[() => props.organisation, modelValue],
([, isOpen]) => {
if (isOpen) {
form.value = initialState()
errors.value = {}
}
},
{ immediate: true },
)
function copySlug() {
if (form.value.slug) {
navigator.clipboard.writeText(form.value.slug)
showCopied.value = true
}
}
function onSubmit() {
refVForm.value?.validate().then(({ valid }) => {
@@ -29,24 +65,32 @@ function onSubmit() {
errors.value = {}
updateOrganisation(
{ id: props.organisation.id, name: name.value },
{
onSuccess: () => {
modelValue.value = false
showSuccess.value = true
},
onError: (err: Error) => {
const data = (err as AxiosError<ApiErrorResponse>).response?.data
if (data?.errors) {
errors.value = { name: data.errors.name?.[0] ?? '' }
}
else if (data?.message) {
errors.value = { name: data.message }
}
},
const payload = {
id: props.organisation.id,
name: form.value.name,
slug: form.value.slug,
contact_name: form.value.contact_name || null,
contact_email: form.value.contact_email || null,
phone: form.value.phone || null,
website: form.value.website || null,
}
updateOrganisation(payload, {
onSuccess: () => {
modelValue.value = false
showSuccess.value = true
},
)
onError: (err: Error) => {
const data = (err as AxiosError<ApiErrorResponse>).response?.data
if (data?.errors) {
const fieldErrors: Record<string, string> = {}
for (const [key, messages] of Object.entries(data.errors)) {
fieldErrors[key] = messages[0] ?? ''
}
errors.value = fieldErrors
}
},
})
})
}
</script>
@@ -54,27 +98,108 @@ function onSubmit() {
<template>
<VDialog
v-model="modelValue"
max-width="450"
max-width="640"
>
<VCard title="Naam bewerken">
<VCard title="Organisatiegegevens bewerken">
<VForm
ref="refVForm"
@submit.prevent="onSubmit"
>
<VCardText>
<AppTextField
v-model="name"
label="Organisatienaam"
:rules="[requiredValidator]"
:error-messages="errors.name"
autofocus
autocomplete="one-time-code"
/>
<VRow>
<VCol
cols="12"
md="6"
>
<AppTextField
v-model="form.name"
label="Organisatienaam"
:rules="[requiredValidator]"
:error-messages="errors.name"
autofocus
/>
</VCol>
<VCol
cols="12"
md="6"
>
<AppTextField
v-model="form.slug"
label="Slug"
:rules="[requiredValidator, slugRegexValidator]"
:error-messages="errors.slug"
>
<template #append-inner>
<VTooltip text="URL-veilige identificatie — wijzigen kan bestaande links breken">
<template #activator="{ props: tooltipProps }">
<VIcon
v-bind="tooltipProps"
icon="tabler-info-circle"
size="18"
color="disabled"
/>
</template>
</VTooltip>
<VBtn
icon="tabler-copy"
variant="text"
size="x-small"
@click="copySlug"
/>
</template>
</AppTextField>
</VCol>
<VCol
cols="12"
md="6"
>
<AppTextField
v-model="form.contact_name"
label="Contactpersoon"
:error-messages="errors.contact_name"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<AppTextField
v-model="form.contact_email"
label="Contact e-mail"
type="email"
:rules="form.contact_email ? [emailValidator] : []"
:error-messages="errors.contact_email"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<AppTextField
v-model="form.phone"
label="Telefoon"
:error-messages="errors.phone"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<AppTextField
v-model="form.website"
label="Website"
placeholder="https://example.com"
:rules="form.website ? [urlValidator] : []"
:error-messages="errors.website"
/>
</VCol>
</VRow>
</VCardText>
<VCardActions>
<VSpacer />
<VBtn
variant="text"
:disabled="isPending"
@click="modelValue = false"
>
Annuleren
@@ -84,7 +209,7 @@ function onSubmit() {
color="primary"
:loading="isPending"
>
Opslaan
Wijzigingen opslaan
</VBtn>
</VCardActions>
</VForm>
@@ -96,6 +221,13 @@ function onSubmit() {
color="success"
:timeout="3000"
>
Naam bijgewerkt
Organisatie bijgewerkt
</VSnackbar>
<VSnackbar
v-model="showCopied"
color="info"
:timeout="2000"
>
Slug gekopieerd
</VSnackbar>
</template>