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