feat: email infrastructure frontend — settings, templates, and log tabs

Adds three new tabs to the organisation settings page:

- E-mail opmaak: replaces old EmailBrandingTab to use the new
  organisation_email_settings API (logo, colors, footer, reply-to)
- E-mail templates: list/edit/preview/test/reset all 6 template types
  with variable hints, defaults comparison, and iframe preview
- E-mail log: server-side paginated table with filters (search, status,
  type, date range), status chips, and expandable row details

Supporting files:
- types/email.ts: TypeScript interfaces for settings, templates, logs
- composables/api/useEmail.ts: TanStack Query hooks for all email endpoints

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-15 20:28:38 +02:00
parent 65978104d8
commit df68aa8aef
7 changed files with 1211 additions and 76 deletions

View File

@@ -0,0 +1,164 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
import type { Ref } from 'vue'
import { apiClient } from '@/lib/axios'
import type {
EmailLog,
EmailLogFilters,
EmailSettings,
EmailSettingsDefaults,
EmailTemplate,
UpdateEmailSettingsPayload,
UpdateEmailTemplatePayload,
} from '@/types/email'
interface ApiResponse<T> {
success: boolean
data: T
message?: string
}
interface PaginatedResponse<T> {
data: T[]
links: Record<string, string | null>
meta: {
current_page: number
per_page: number
total: number
last_page: number
}
}
// ── Email Settings ──
export function useEmailSettings(orgId: Ref<string>) {
return useQuery({
queryKey: ['email-settings', orgId],
queryFn: async () => {
const { data } = await apiClient.get<ApiResponse<EmailSettings | EmailSettingsDefaults>>(
`/organisations/${orgId.value}/email-settings`,
)
return data.data
},
enabled: () => !!orgId.value,
})
}
export function useUpdateEmailSettings(orgId: Ref<string>) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (payload: UpdateEmailSettingsPayload) => {
const { data } = await apiClient.put<ApiResponse<EmailSettings>>(
`/organisations/${orgId.value}/email-settings`,
payload,
)
return data.data
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['email-settings', orgId.value] })
},
})
}
// ── Email Templates ──
export function useEmailTemplates(orgId: Ref<string>) {
return useQuery({
queryKey: ['email-templates', orgId],
queryFn: async () => {
const { data } = await apiClient.get<ApiResponse<EmailTemplate[]>>(
`/organisations/${orgId.value}/email-templates`,
)
return data.data
},
enabled: () => !!orgId.value,
})
}
export function useUpdateEmailTemplate(orgId: Ref<string>) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({ type, ...payload }: UpdateEmailTemplatePayload & { type: string }) => {
const { data } = await apiClient.put<ApiResponse<EmailTemplate>>(
`/organisations/${orgId.value}/email-templates/${type}`,
payload,
)
return data.data
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['email-templates', orgId.value] })
},
})
}
export function useResetEmailTemplate(orgId: Ref<string>) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (type: string) => {
await apiClient.delete(`/organisations/${orgId.value}/email-templates/${type}`)
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['email-templates', orgId.value] })
},
})
}
export function usePreviewEmailTemplate(orgId: Ref<string>) {
return useMutation({
mutationFn: async (type: string) => {
const { data } = await apiClient.post<ApiResponse<{ html: string }>>(
`/organisations/${orgId.value}/email-templates/${type}/preview`,
)
return data.data.html
},
})
}
export function useSendTestEmail(orgId: Ref<string>) {
return useMutation({
mutationFn: async ({ type, email }: { type: string; email: string }) => {
const { data } = await apiClient.post<ApiResponse<null>>(
`/organisations/${orgId.value}/email-templates/${type}/send-test`,
{ email },
)
return data
},
})
}
// ── Email Logs ──
export function useEmailLogs(orgId: Ref<string>, filters: Ref<EmailLogFilters>) {
return useQuery({
queryKey: ['email-logs', orgId, filters],
queryFn: async () => {
const params: Record<string, string | number> = {
page: filters.value.page,
per_page: filters.value.perPage,
}
if (filters.value.search) params.search = filters.value.search
if (filters.value.status) params.status = filters.value.status
if (filters.value.templateType) params.template_type = filters.value.templateType
if (filters.value.eventId) params.event_id = filters.value.eventId
if (filters.value.from) params.from = filters.value.from
if (filters.value.to) params.to = filters.value.to
const { data } = await apiClient.get<ApiResponse<PaginatedResponse<EmailLog>>>(
`/organisations/${orgId.value}/email-logs`,
{ params },
)
return data.data
},
enabled: () => !!orgId.value,
})
}