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:
242
apps/app/src/components/organisation/EmailBrandingTab.vue
Normal file
242
apps/app/src/components/organisation/EmailBrandingTab.vue
Normal 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>
|
||||||
Reference in New Issue
Block a user