feat: add EmailBrandingTab component for organisation email branding

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-14 19:04:13 +02:00
parent ae7ba63822
commit 185637fa50

View File

@@ -0,0 +1,242 @@
<script setup lang="ts">
import { VForm } from 'vuetify/components/VForm'
import { useOrganisationDetail, useUpdateOrganisation } from '@/composables/api/useOrganisations'
import { emailValidator } from '@core/utils/validators'
const props = defineProps<{
orgId: string
}>()
const orgIdRef = computed(() => props.orgId)
const { data: organisation } = useOrganisationDetail(orgIdRef)
const { mutate: updateOrg, isPending: isSaving } = useUpdateOrganisation()
const form = ref<InstanceType<typeof VForm>>()
const snackbar = ref(false)
const emailLogoUrl = ref<string>('')
const emailPrimaryColor = ref<string>('')
const emailReplyTo = ref<string>('')
const emailSenderName = ref<string>('')
const emailFooterText = ref<string>('')
const serverErrors = ref<Record<string, string[]>>({})
const showColorPicker = ref(false)
const isDev = import.meta.env.DEV
watch(organisation, org => {
if (org) {
emailLogoUrl.value = org.email_logo_url ?? ''
emailPrimaryColor.value = org.email_primary_color ?? ''
emailReplyTo.value = org.email_reply_to ?? ''
emailSenderName.value = org.email_sender_name ?? ''
emailFooterText.value = org.email_footer_text ?? ''
}
}, { immediate: true })
const hexColorValidator = (value: unknown) => {
if (!value || (typeof value === 'string' && value.trim() === ''))
return true
return /^#[0-9a-fA-F]{6}$/.test(String(value)) || 'Ongeldige hex-kleur (bijv. #6366f1)'
}
const optionalEmailValidator = (value: unknown) => {
if (!value || (typeof value === 'string' && value.trim() === ''))
return true
return emailValidator(value)
}
const apiBaseUrl = import.meta.env.VITE_API_URL?.replace(/\/api\/v1\/?$/, '') ?? ''
async function onSubmit() {
const { valid } = await form.value!.validate()
if (!valid)
return
serverErrors.value = {}
updateOrg({
id: props.orgId,
email_logo_url: emailLogoUrl.value || null,
email_primary_color: emailPrimaryColor.value || null,
email_reply_to: emailReplyTo.value || null,
email_sender_name: emailSenderName.value || null,
email_footer_text: emailFooterText.value || null,
}, {
onSuccess: () => {
snackbar.value = true
},
onError: (error: any) => {
if (error.response?.status === 422 && error.response?.data?.errors) {
serverErrors.value = error.response.data.errors
}
},
})
}
function fieldErrors(field: string): string | undefined {
return serverErrors.value[field]?.[0]
}
</script>
<template>
<VCard>
<VCardTitle>E-mail opmaak</VCardTitle>
<VCardSubtitle>Pas het uiterlijk van uitgaande e-mails aan</VCardSubtitle>
<VCardText>
<VForm
ref="form"
@submit.prevent="onSubmit"
>
<VRow>
<VCol
cols="12"
md="8"
>
<VTextField
v-model="emailLogoUrl"
label="Logo URL"
hint="URL naar je organisatielogo (wordt getoond in de e-mailheader)"
persistent-hint
:error-messages="fieldErrors('email_logo_url')"
/>
<img
v-if="emailLogoUrl"
:src="emailLogoUrl"
style="max-height: 48px"
class="mt-2"
@error="($event.target as HTMLImageElement).style.display = 'none'"
@load="($event.target as HTMLImageElement).style.display = 'block'"
>
</VCol>
<VCol
cols="12"
md="8"
>
<div class="d-flex align-center gap-3">
<VMenu
v-model="showColorPicker"
:close-on-content-click="false"
location="bottom start"
>
<template #activator="{ props: menuProps }">
<div
v-bind="menuProps"
class="color-swatch rounded cursor-pointer border"
:style="{ backgroundColor: emailPrimaryColor || '#6366f1' }"
/>
</template>
<VCard>
<VColorPicker
v-model="emailPrimaryColor"
mode="hex"
:modes="['hex']"
/>
</VCard>
</VMenu>
<VTextField
v-model="emailPrimaryColor"
label="Primaire kleur"
placeholder="#6366f1"
:rules="[hexColorValidator]"
:error-messages="fieldErrors('email_primary_color')"
class="flex-grow-1"
/>
</div>
</VCol>
<VCol
cols="12"
md="8"
>
<VTextField
v-model="emailSenderName"
label="Afzendernaam"
hint="De 'Van'-naam in uitgaande e-mails. Leeg = organisatienaam"
persistent-hint
:placeholder="organisation?.name"
:error-messages="fieldErrors('email_sender_name')"
/>
</VCol>
<VCol
cols="12"
md="8"
>
<VTextField
v-model="emailReplyTo"
label="Reply-to e-mailadres"
hint="Antwoorden op e-mails worden naar dit adres gestuurd"
persistent-hint
:rules="[optionalEmailValidator]"
:error-messages="fieldErrors('email_reply_to')"
/>
</VCol>
<VCol
cols="12"
md="8"
>
<VTextarea
v-model="emailFooterText"
label="Footertekst"
hint="Wordt onderaan elke e-mail getoond (bijv. contactgegevens, adres)"
persistent-hint
:rows="3"
:counter="2000"
:maxlength="2000"
:error-messages="fieldErrors('email_footer_text')"
/>
</VCol>
<VCol
cols="12"
md="8"
>
<VBtn
type="submit"
color="primary"
:loading="isSaving"
>
Opslaan
</VBtn>
<div
v-if="isDev"
class="mt-3"
>
<a
:href="`${apiBaseUrl}/mail-preview/registration-confirmation`"
target="_blank"
class="text-caption text-medium-emphasis"
>
📧 Bekijk een voorbeeld-email
</a>
</div>
</VCol>
</VRow>
</VForm>
</VCardText>
<VSnackbar
v-model="snackbar"
color="success"
:timeout="3000"
>
E-mailinstellingen opgeslagen
</VSnackbar>
</VCard>
</template>
<style scoped>
.color-swatch {
inline-size: 40px;
block-size: 40px;
min-inline-size: 40px;
}
</style>