diff --git a/admin-compliance/app/sdk/email-templates/_components/EditorTab.tsx b/admin-compliance/app/sdk/email-templates/_components/EditorTab.tsx
new file mode 100644
index 0000000..441d0cc
--- /dev/null
+++ b/admin-compliance/app/sdk/email-templates/_components/EditorTab.tsx
@@ -0,0 +1,131 @@
+'use client'
+
+import { CATEGORIES, EmailTemplate, STATUS_BADGE, TemplateVersion } from '../_types'
+
+interface EditorTabProps {
+ template: EmailTemplate | null
+ version: TemplateVersion | null
+ subject: string
+ html: string
+ previewHtml: string | null
+ saving: boolean
+ onSubjectChange: (v: string) => void
+ onHtmlChange: (v: string) => void
+ onSave: () => void
+ onPublish: () => void
+ onPreview: () => void
+ onBack: () => void
+}
+
+export function EditorTab({
+ template, version, subject, html, previewHtml, saving,
+ onSubjectChange, onHtmlChange, onSave, onPublish, onPreview, onBack,
+}: EditorTabProps) {
+ if (!template) {
+ return (
+
+ Waehlen Sie ein Template aus der Liste.
+
+
+
+ )
+ }
+
+ const cat = CATEGORIES[template.category] || CATEGORIES.general
+
+ return (
+
+
+
+
+
{template.name}
+ {cat.label}
+ {version && (
+
+ {(STATUS_BADGE[version.status] || STATUS_BADGE.draft).label}
+
+ )}
+
+
+
+ {version && version.status !== 'published' && (
+
+ )}
+ {version && (
+
+ )}
+
+
+
+ {/* Variables */}
+
+ Variablen:
+ {(template.variables || []).map(v => (
+
+ ))}
+
+
+ {/* Split View */}
+
+ {/* Editor */}
+
+
+
+ onSubjectChange(e.target.value)}
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent"
+ placeholder="E-Mail Betreff..."
+ />
+
+
+
+
+ {/* Preview */}
+
+
+
+ {previewHtml ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ )
+}
diff --git a/admin-compliance/app/sdk/email-templates/_components/LogsTab.tsx b/admin-compliance/app/sdk/email-templates/_components/LogsTab.tsx
new file mode 100644
index 0000000..5a6a897
--- /dev/null
+++ b/admin-compliance/app/sdk/email-templates/_components/LogsTab.tsx
@@ -0,0 +1,55 @@
+'use client'
+
+import { SendLog } from '../_types'
+
+interface LogsTabProps {
+ logs: SendLog[]
+ total: number
+}
+
+export function LogsTab({ logs, total }: LogsTabProps) {
+ return (
+
+
E-Mail-Verlauf ({total})
+
+ {logs.length === 0 ? (
+
Noch keine E-Mails gesendet.
+ ) : (
+
+
+
+
+ | Typ |
+ Empfaenger |
+ Betreff |
+ Status |
+ Datum |
+
+
+
+ {logs.map(log => (
+
+ | {log.template_type} |
+ {log.recipient} |
+ {log.subject} |
+
+
+ {log.status}
+
+ |
+
+ {log.sent_at ? new Date(log.sent_at).toLocaleString('de-DE') : '-'}
+ |
+
+ ))}
+
+
+
+ )}
+
+ )
+}
diff --git a/admin-compliance/app/sdk/email-templates/_components/SettingsTab.tsx b/admin-compliance/app/sdk/email-templates/_components/SettingsTab.tsx
new file mode 100644
index 0000000..4b37a5f
--- /dev/null
+++ b/admin-compliance/app/sdk/email-templates/_components/SettingsTab.tsx
@@ -0,0 +1,143 @@
+'use client'
+
+import { Settings } from '../_types'
+
+interface SettingsTabProps {
+ settings: Settings
+ saving: boolean
+ onChange: (s: Settings) => void
+ onSave: () => void
+}
+
+export function SettingsTab({ settings, saving, onChange, onSave }: SettingsTabProps) {
+ const update = (field: keyof Settings, value: string) => {
+ onChange({ ...settings, [field]: value })
+ }
+
+ return (
+
+
E-Mail-Einstellungen
+
+
+
+
+
+
+ update('reply_to', e.target.value)}
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm"
+ placeholder="optional"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/admin-compliance/app/sdk/email-templates/_components/TabNav.tsx b/admin-compliance/app/sdk/email-templates/_components/TabNav.tsx
new file mode 100644
index 0000000..f4750fe
--- /dev/null
+++ b/admin-compliance/app/sdk/email-templates/_components/TabNav.tsx
@@ -0,0 +1,43 @@
+'use client'
+
+import { TabId } from '../_types'
+
+interface TabNavProps {
+ activeTab: TabId
+ onTabChange: (tab: TabId) => void
+ logsTotal: number
+}
+
+const TABS: { id: TabId; label: string }[] = [
+ { id: 'templates', label: 'Templates' },
+ { id: 'editor', label: 'Editor' },
+ { id: 'settings', label: 'Einstellungen' },
+ { id: 'logs', label: 'Logs' },
+]
+
+export function TabNav({ activeTab, onTabChange, logsTotal }: TabNavProps) {
+ return (
+
+
+
+ )
+}
diff --git a/admin-compliance/app/sdk/email-templates/_components/TemplateCard.tsx b/admin-compliance/app/sdk/email-templates/_components/TemplateCard.tsx
new file mode 100644
index 0000000..ca6fee0
--- /dev/null
+++ b/admin-compliance/app/sdk/email-templates/_components/TemplateCard.tsx
@@ -0,0 +1,49 @@
+'use client'
+
+import { CATEGORIES, EmailTemplate, STATUS_BADGE } from '../_types'
+
+interface TemplateCardProps {
+ template: EmailTemplate
+ onEdit: () => void
+}
+
+export function TemplateCard({ template, onEdit }: TemplateCardProps) {
+ const cat = CATEGORIES[template.category] || CATEGORIES.general
+ const version = template.latest_version
+ const status = version ? (STATUS_BADGE[version.status] || STATUS_BADGE.draft) : STATUS_BADGE.draft
+
+ return (
+
+
+
+
{template.name}
+
+ {cat.label}
+
+
+
{status.label}
+
+ {template.description && (
+
{template.description}
+ )}
+
+ {(template.variables || []).slice(0, 4).map(v => (
+
+ {`{{${v}}}`}
+
+ ))}
+ {(template.variables || []).length > 4 && (
+ +{template.variables.length - 4}
+ )}
+
+ {version && (
+
+ v{version.version} · {version.created_at ? new Date(version.created_at).toLocaleDateString('de-DE') : ''}
+
+ )}
+
+ )
+}
diff --git a/admin-compliance/app/sdk/email-templates/_components/TemplatesTab.tsx b/admin-compliance/app/sdk/email-templates/_components/TemplatesTab.tsx
new file mode 100644
index 0000000..5e30fac
--- /dev/null
+++ b/admin-compliance/app/sdk/email-templates/_components/TemplatesTab.tsx
@@ -0,0 +1,64 @@
+'use client'
+
+import { CATEGORIES, EmailTemplate } from '../_types'
+import { TemplateCard } from './TemplateCard'
+
+interface TemplatesTabProps {
+ templates: EmailTemplate[]
+ loading: boolean
+ selectedCategory: string | null
+ onCategoryChange: (cat: string | null) => void
+ onEdit: (t: EmailTemplate) => void
+ onInitialize: () => void
+}
+
+export function TemplatesTab({
+ templates, loading, selectedCategory, onCategoryChange, onEdit, onInitialize,
+}: TemplatesTabProps) {
+ return (
+
+ {/* Category Pills */}
+
+
+ {Object.entries(CATEGORIES).map(([key, cat]) => (
+
+ ))}
+
+
+ {loading ? (
+
Lade Templates...
+ ) : templates.length === 0 ? (
+
+
Keine Templates vorhanden.
+
+
+ ) : (
+
+ {templates.map(t => (
+ onEdit(t)} />
+ ))}
+
+ )}
+
+ )
+}
diff --git a/admin-compliance/app/sdk/email-templates/_hooks/useEmailTemplates.ts b/admin-compliance/app/sdk/email-templates/_hooks/useEmailTemplates.ts
new file mode 100644
index 0000000..b51b6d6
--- /dev/null
+++ b/admin-compliance/app/sdk/email-templates/_hooks/useEmailTemplates.ts
@@ -0,0 +1,227 @@
+'use client'
+
+import { useCallback, useEffect, useState } from 'react'
+import {
+ API_BASE,
+ EmailTemplate,
+ SendLog,
+ Settings,
+ TabId,
+ TemplateType,
+ TemplateVersion,
+ getHeaders,
+} from '../_types'
+
+export function useEmailTemplates(activeTab: TabId) {
+ const [templates, setTemplates] = useState([])
+ const [templateTypes, setTemplateTypes] = useState([])
+ const [settings, setSettings] = useState(null)
+ const [logs, setLogs] = useState([])
+ const [logsTotal, setLogsTotal] = useState(0)
+ const [loading, setLoading] = useState(true)
+ const [error, setError] = useState(null)
+ const [selectedCategory, setSelectedCategory] = useState(null)
+
+ // Editor state
+ const [selectedTemplate, setSelectedTemplate] = useState(null)
+ const [editorSubject, setEditorSubject] = useState('')
+ const [editorHtml, setEditorHtml] = useState('')
+ const [editorVersion, setEditorVersion] = useState(null)
+ const [saving, setSaving] = useState(false)
+ const [previewHtml, setPreviewHtml] = useState(null)
+
+ // Settings form
+ const [settingsForm, setSettingsForm] = useState(null)
+ const [savingSettings, setSavingSettings] = useState(false)
+
+ const loadTemplates = useCallback(async () => {
+ try {
+ const url = selectedCategory ? `${API_BASE}?category=${selectedCategory}` : API_BASE
+ const res = await fetch(url, { headers: getHeaders() })
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
+ const data = await res.json()
+ setTemplates(Array.isArray(data) ? data : [])
+ } catch (e: any) {
+ setError(e.message)
+ }
+ }, [selectedCategory])
+
+ const loadTypes = useCallback(async () => {
+ try {
+ const res = await fetch(`${API_BASE}/types`, { headers: getHeaders() })
+ if (res.ok) {
+ const data = await res.json()
+ setTemplateTypes(Array.isArray(data) ? data : [])
+ }
+ } catch { /* ignore */ }
+ }, [])
+
+ const loadSettings = useCallback(async () => {
+ try {
+ const res = await fetch(`${API_BASE}/settings`, { headers: getHeaders() })
+ if (res.ok) {
+ const data = await res.json()
+ setSettings(data)
+ setSettingsForm(data)
+ }
+ } catch { /* ignore */ }
+ }, [])
+
+ const loadLogs = useCallback(async () => {
+ try {
+ const res = await fetch(`${API_BASE}/logs?limit=50`, { headers: getHeaders() })
+ if (res.ok) {
+ const data = await res.json()
+ setLogs(data.logs || [])
+ setLogsTotal(data.total || 0)
+ }
+ } catch { /* ignore */ }
+ }, [])
+
+ useEffect(() => {
+ setLoading(true)
+ Promise.all([loadTemplates(), loadTypes(), loadSettings()])
+ .finally(() => setLoading(false))
+ }, [loadTemplates, loadTypes, loadSettings])
+
+ useEffect(() => {
+ if (activeTab === 'logs') loadLogs()
+ }, [activeTab, loadLogs])
+
+ useEffect(() => {
+ loadTemplates()
+ }, [selectedCategory, loadTemplates])
+
+ const openEditor = useCallback(async (template: EmailTemplate) => {
+ setSelectedTemplate(template)
+ setPreviewHtml(null)
+
+ if (template.latest_version) {
+ setEditorSubject(template.latest_version.subject)
+ setEditorHtml(template.latest_version.body_html)
+ setEditorVersion(template.latest_version)
+ } else {
+ // Load default content
+ try {
+ const res = await fetch(`${API_BASE}/default/${template.template_type}`, { headers: getHeaders() })
+ if (res.ok) {
+ const data = await res.json()
+ setEditorSubject(data.default_subject || '')
+ setEditorHtml(data.default_body_html || '')
+ }
+ } catch { /* ignore */ }
+ setEditorVersion(null)
+ }
+ }, [])
+
+ const saveVersion = useCallback(async () => {
+ if (!selectedTemplate) return
+ setSaving(true)
+ try {
+ const res = await fetch(`${API_BASE}/${selectedTemplate.id}/versions`, {
+ method: 'POST',
+ headers: getHeaders(),
+ body: JSON.stringify({
+ version: editorVersion ? `${parseFloat(editorVersion.version) + 0.1}` : '1.0',
+ language: 'de',
+ subject: editorSubject,
+ body_html: editorHtml,
+ }),
+ })
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
+ const version = await res.json()
+ setEditorVersion(version)
+ await loadTemplates()
+ } catch (e: any) {
+ setError(e.message)
+ } finally {
+ setSaving(false)
+ }
+ }, [selectedTemplate, editorSubject, editorHtml, editorVersion, loadTemplates])
+
+ const publishVersion = useCallback(async () => {
+ if (!editorVersion) return
+ setSaving(true)
+ try {
+ const res = await fetch(`${API_BASE}/versions/${editorVersion.id}/publish`, {
+ method: 'POST',
+ headers: getHeaders(),
+ })
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
+ const updated = await res.json()
+ setEditorVersion(updated)
+ await loadTemplates()
+ } catch (e: any) {
+ setError(e.message)
+ } finally {
+ setSaving(false)
+ }
+ }, [editorVersion, loadTemplates])
+
+ const loadPreview = useCallback(async () => {
+ if (!editorVersion) return
+ try {
+ const res = await fetch(`${API_BASE}/versions/${editorVersion.id}/preview`, {
+ method: 'POST',
+ headers: getHeaders(),
+ body: JSON.stringify({ variables: {} }),
+ })
+ if (res.ok) {
+ const data = await res.json()
+ setPreviewHtml(data.body_html)
+ }
+ } catch { /* ignore */ }
+ }, [editorVersion])
+
+ const saveSettings2 = useCallback(async () => {
+ if (!settingsForm) return
+ setSavingSettings(true)
+ try {
+ const res = await fetch(`${API_BASE}/settings`, {
+ method: 'PUT',
+ headers: getHeaders(),
+ body: JSON.stringify(settingsForm),
+ })
+ if (res.ok) {
+ const data = await res.json()
+ setSettings(data)
+ setSettingsForm(data)
+ }
+ } catch (e: any) {
+ setError(e.message)
+ } finally {
+ setSavingSettings(false)
+ }
+ }, [settingsForm])
+
+ const initializeDefaults = useCallback(async () => {
+ try {
+ const res = await fetch(`${API_BASE}/initialize`, {
+ method: 'POST',
+ headers: getHeaders(),
+ })
+ if (res.ok) {
+ await loadTemplates()
+ }
+ } catch (e: any) {
+ setError(e.message)
+ }
+ }, [loadTemplates])
+
+ return {
+ // Data
+ templates, templateTypes, settings, logs, logsTotal, loading, error,
+ selectedCategory,
+ // Editor
+ selectedTemplate, editorSubject, editorHtml, editorVersion, saving, previewHtml,
+ // Settings
+ settingsForm, savingSettings,
+ // Setters
+ setError, setSelectedCategory,
+ setEditorSubject, setEditorHtml,
+ setSettingsForm,
+ // Actions
+ openEditor, saveVersion, publishVersion, loadPreview,
+ saveSettings2, initializeDefaults,
+ }
+}
diff --git a/admin-compliance/app/sdk/email-templates/_types.ts b/admin-compliance/app/sdk/email-templates/_types.ts
new file mode 100644
index 0000000..417121f
--- /dev/null
+++ b/admin-compliance/app/sdk/email-templates/_types.ts
@@ -0,0 +1,84 @@
+export interface EmailTemplate {
+ id: string
+ template_type: string
+ name: string
+ description: string | null
+ category: string
+ is_active: boolean
+ sort_order: number
+ variables: string[]
+ latest_version?: TemplateVersion | null
+ created_at: string | null
+ updated_at: string | null
+}
+
+export interface TemplateVersion {
+ id: string
+ template_id: string
+ version: string
+ language: string
+ subject: string
+ body_html: string
+ body_text: string | null
+ status: string
+ submitted_at: string | null
+ published_at: string | null
+ created_at: string | null
+}
+
+export interface TemplateType {
+ type: string
+ name: string
+ category: string
+ variables: string[]
+}
+
+export interface SendLog {
+ id: string
+ template_type: string
+ recipient: string
+ subject: string
+ status: string
+ sent_at: string | null
+}
+
+export interface Settings {
+ sender_name: string
+ sender_email: string
+ reply_to: string | null
+ logo_url: string | null
+ primary_color: string
+ secondary_color: string
+ footer_text: string
+ company_name: string | null
+ company_address: string | null
+}
+
+export type TabId = 'templates' | 'editor' | 'settings' | 'logs'
+
+export const API_BASE = '/api/sdk/v1/compliance/email-templates'
+
+export function getHeaders(): HeadersInit {
+ if (typeof window === 'undefined') return { 'Content-Type': 'application/json' }
+ return {
+ 'Content-Type': 'application/json',
+ 'X-Tenant-ID': localStorage.getItem('bp_tenant_id') || '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e',
+ 'X-User-ID': localStorage.getItem('bp_user_id') || '',
+ }
+}
+
+export const CATEGORIES: Record = {
+ general: { label: 'Allgemein', color: 'text-gray-700', bgColor: 'bg-gray-100' },
+ dsr: { label: 'Betroffenenrechte', color: 'text-blue-700', bgColor: 'bg-blue-100' },
+ consent: { label: 'Einwilligung', color: 'text-green-700', bgColor: 'bg-green-100' },
+ breach: { label: 'Datenpanne', color: 'text-red-700', bgColor: 'bg-red-100' },
+ vendor: { label: 'Dienstleister', color: 'text-purple-700', bgColor: 'bg-purple-100' },
+ training: { label: 'Schulung', color: 'text-orange-700', bgColor: 'bg-orange-100' },
+}
+
+export const STATUS_BADGE: Record = {
+ draft: { label: 'Entwurf', color: 'bg-gray-100 text-gray-700' },
+ review: { label: 'Pruefung', color: 'bg-yellow-100 text-yellow-700' },
+ approved: { label: 'Genehmigt', color: 'bg-blue-100 text-blue-700' },
+ published: { label: 'Publiziert', color: 'bg-green-100 text-green-700' },
+}
diff --git a/admin-compliance/app/sdk/email-templates/page.tsx b/admin-compliance/app/sdk/email-templates/page.tsx
index 99a7a76..32ef2c0 100644
--- a/admin-compliance/app/sdk/email-templates/page.tsx
+++ b/admin-compliance/app/sdk/email-templates/page.tsx
@@ -1,323 +1,34 @@
'use client'
-import React, { useState, useEffect, useCallback } from 'react'
-import { useSDK } from '@/lib/sdk'
+import { useState } from 'react'
import { StepHeader } from '@/components/sdk/StepHeader'
-// =============================================================================
-// TYPES
-// =============================================================================
-
-interface EmailTemplate {
- id: string
- template_type: string
- name: string
- description: string | null
- category: string
- is_active: boolean
- sort_order: number
- variables: string[]
- latest_version?: TemplateVersion | null
- created_at: string | null
- updated_at: string | null
-}
-
-interface TemplateVersion {
- id: string
- template_id: string
- version: string
- language: string
- subject: string
- body_html: string
- body_text: string | null
- status: string
- submitted_at: string | null
- published_at: string | null
- created_at: string | null
-}
-
-interface TemplateType {
- type: string
- name: string
- category: string
- variables: string[]
-}
-
-interface SendLog {
- id: string
- template_type: string
- recipient: string
- subject: string
- status: string
- sent_at: string | null
-}
-
-interface Settings {
- sender_name: string
- sender_email: string
- reply_to: string | null
- logo_url: string | null
- primary_color: string
- secondary_color: string
- footer_text: string
- company_name: string | null
- company_address: string | null
-}
-
-type TabId = 'templates' | 'editor' | 'settings' | 'logs'
-
-const API_BASE = '/api/sdk/v1/compliance/email-templates'
-
-function getHeaders(): HeadersInit {
- if (typeof window === 'undefined') return { 'Content-Type': 'application/json' }
- return {
- 'Content-Type': 'application/json',
- 'X-Tenant-ID': localStorage.getItem('bp_tenant_id') || '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e',
- 'X-User-ID': localStorage.getItem('bp_user_id') || '',
- }
-}
-
-// =============================================================================
-// CATEGORY CONFIG
-// =============================================================================
-
-const CATEGORIES: Record = {
- general: { label: 'Allgemein', color: 'text-gray-700', bgColor: 'bg-gray-100' },
- dsr: { label: 'Betroffenenrechte', color: 'text-blue-700', bgColor: 'bg-blue-100' },
- consent: { label: 'Einwilligung', color: 'text-green-700', bgColor: 'bg-green-100' },
- breach: { label: 'Datenpanne', color: 'text-red-700', bgColor: 'bg-red-100' },
- vendor: { label: 'Dienstleister', color: 'text-purple-700', bgColor: 'bg-purple-100' },
- training: { label: 'Schulung', color: 'text-orange-700', bgColor: 'bg-orange-100' },
-}
-
-const STATUS_BADGE: Record = {
- draft: { label: 'Entwurf', color: 'bg-gray-100 text-gray-700' },
- review: { label: 'Pruefung', color: 'bg-yellow-100 text-yellow-700' },
- approved: { label: 'Genehmigt', color: 'bg-blue-100 text-blue-700' },
- published: { label: 'Publiziert', color: 'bg-green-100 text-green-700' },
-}
-
-// =============================================================================
-// PAGE
-// =============================================================================
+import { EmailTemplate, TabId } from './_types'
+import { useEmailTemplates } from './_hooks/useEmailTemplates'
+import { TabNav } from './_components/TabNav'
+import { TemplatesTab } from './_components/TemplatesTab'
+import { EditorTab } from './_components/EditorTab'
+import { SettingsTab } from './_components/SettingsTab'
+import { LogsTab } from './_components/LogsTab'
export default function EmailTemplatesPage() {
- const sdk = useSDK()
const [activeTab, setActiveTab] = useState('templates')
- const [templates, setTemplates] = useState([])
- const [templateTypes, setTemplateTypes] = useState([])
- const [settings, setSettings] = useState(null)
- const [logs, setLogs] = useState([])
- const [logsTotal, setLogsTotal] = useState(0)
- const [loading, setLoading] = useState(true)
- const [error, setError] = useState(null)
- const [selectedCategory, setSelectedCategory] = useState(null)
+ const {
+ templates, logs, logsTotal, loading, error,
+ selectedCategory,
+ selectedTemplate, editorSubject, editorHtml, editorVersion, saving, previewHtml,
+ settingsForm, savingSettings,
+ setError, setSelectedCategory,
+ setEditorSubject, setEditorHtml,
+ setSettingsForm,
+ openEditor, saveVersion, publishVersion, loadPreview,
+ saveSettings2, initializeDefaults,
+ } = useEmailTemplates(activeTab)
- // Editor state
- const [selectedTemplate, setSelectedTemplate] = useState(null)
- const [editorSubject, setEditorSubject] = useState('')
- const [editorHtml, setEditorHtml] = useState('')
- const [editorVersion, setEditorVersion] = useState(null)
- const [saving, setSaving] = useState(false)
- const [previewHtml, setPreviewHtml] = useState(null)
-
- // Settings form
- const [settingsForm, setSettingsForm] = useState(null)
- const [savingSettings, setSavingSettings] = useState(false)
-
- // =============================================================================
- // DATA LOADING
- // =============================================================================
-
- const loadTemplates = useCallback(async () => {
- try {
- const url = selectedCategory ? `${API_BASE}?category=${selectedCategory}` : API_BASE
- const res = await fetch(url, { headers: getHeaders() })
- if (!res.ok) throw new Error(`HTTP ${res.status}`)
- const data = await res.json()
- setTemplates(Array.isArray(data) ? data : [])
- } catch (e: any) {
- setError(e.message)
- }
- }, [selectedCategory])
-
- const loadTypes = useCallback(async () => {
- try {
- const res = await fetch(`${API_BASE}/types`, { headers: getHeaders() })
- if (res.ok) {
- const data = await res.json()
- setTemplateTypes(Array.isArray(data) ? data : [])
- }
- } catch { /* ignore */ }
- }, [])
-
- const loadSettings = useCallback(async () => {
- try {
- const res = await fetch(`${API_BASE}/settings`, { headers: getHeaders() })
- if (res.ok) {
- const data = await res.json()
- setSettings(data)
- setSettingsForm(data)
- }
- } catch { /* ignore */ }
- }, [])
-
- const loadLogs = useCallback(async () => {
- try {
- const res = await fetch(`${API_BASE}/logs?limit=50`, { headers: getHeaders() })
- if (res.ok) {
- const data = await res.json()
- setLogs(data.logs || [])
- setLogsTotal(data.total || 0)
- }
- } catch { /* ignore */ }
- }, [])
-
- useEffect(() => {
- setLoading(true)
- Promise.all([loadTemplates(), loadTypes(), loadSettings()])
- .finally(() => setLoading(false))
- }, [loadTemplates, loadTypes, loadSettings])
-
- useEffect(() => {
- if (activeTab === 'logs') loadLogs()
- }, [activeTab, loadLogs])
-
- useEffect(() => {
- loadTemplates()
- }, [selectedCategory, loadTemplates])
-
- // =============================================================================
- // EDITOR ACTIONS
- // =============================================================================
-
- const openEditor = useCallback(async (template: EmailTemplate) => {
- setSelectedTemplate(template)
+ const handleEdit = async (template: EmailTemplate) => {
setActiveTab('editor')
- setPreviewHtml(null)
-
- if (template.latest_version) {
- setEditorSubject(template.latest_version.subject)
- setEditorHtml(template.latest_version.body_html)
- setEditorVersion(template.latest_version)
- } else {
- // Load default content
- try {
- const res = await fetch(`${API_BASE}/default/${template.template_type}`, { headers: getHeaders() })
- if (res.ok) {
- const data = await res.json()
- setEditorSubject(data.default_subject || '')
- setEditorHtml(data.default_body_html || '')
- }
- } catch { /* ignore */ }
- setEditorVersion(null)
- }
- }, [])
-
- const saveVersion = useCallback(async () => {
- if (!selectedTemplate) return
- setSaving(true)
- try {
- const res = await fetch(`${API_BASE}/${selectedTemplate.id}/versions`, {
- method: 'POST',
- headers: getHeaders(),
- body: JSON.stringify({
- version: editorVersion ? `${parseFloat(editorVersion.version) + 0.1}` : '1.0',
- language: 'de',
- subject: editorSubject,
- body_html: editorHtml,
- }),
- })
- if (!res.ok) throw new Error(`HTTP ${res.status}`)
- const version = await res.json()
- setEditorVersion(version)
- await loadTemplates()
- } catch (e: any) {
- setError(e.message)
- } finally {
- setSaving(false)
- }
- }, [selectedTemplate, editorSubject, editorHtml, editorVersion, loadTemplates])
-
- const publishVersion = useCallback(async () => {
- if (!editorVersion) return
- setSaving(true)
- try {
- const res = await fetch(`${API_BASE}/versions/${editorVersion.id}/publish`, {
- method: 'POST',
- headers: getHeaders(),
- })
- if (!res.ok) throw new Error(`HTTP ${res.status}`)
- const updated = await res.json()
- setEditorVersion(updated)
- await loadTemplates()
- } catch (e: any) {
- setError(e.message)
- } finally {
- setSaving(false)
- }
- }, [editorVersion, loadTemplates])
-
- const loadPreview = useCallback(async () => {
- if (!editorVersion) return
- try {
- const res = await fetch(`${API_BASE}/versions/${editorVersion.id}/preview`, {
- method: 'POST',
- headers: getHeaders(),
- body: JSON.stringify({ variables: {} }),
- })
- if (res.ok) {
- const data = await res.json()
- setPreviewHtml(data.body_html)
- }
- } catch { /* ignore */ }
- }, [editorVersion])
-
- const saveSettings2 = useCallback(async () => {
- if (!settingsForm) return
- setSavingSettings(true)
- try {
- const res = await fetch(`${API_BASE}/settings`, {
- method: 'PUT',
- headers: getHeaders(),
- body: JSON.stringify(settingsForm),
- })
- if (res.ok) {
- const data = await res.json()
- setSettings(data)
- setSettingsForm(data)
- }
- } catch (e: any) {
- setError(e.message)
- } finally {
- setSavingSettings(false)
- }
- }, [settingsForm])
-
- const initializeDefaults = useCallback(async () => {
- try {
- const res = await fetch(`${API_BASE}/initialize`, {
- method: 'POST',
- headers: getHeaders(),
- })
- if (res.ok) {
- await loadTemplates()
- }
- } catch (e: any) {
- setError(e.message)
- }
- }, [loadTemplates])
-
- // =============================================================================
- // RENDER
- // =============================================================================
-
- const tabs: { id: TabId; label: string }[] = [
- { id: 'templates', label: 'Templates' },
- { id: 'editor', label: 'Editor' },
- { id: 'settings', label: 'Einstellungen' },
- { id: 'logs', label: 'Logs' },
- ]
+ await openEditor(template)
+ }
return (
@@ -330,38 +41,15 @@ export default function EmailTemplatesPage() {
)}
- {/* Tab Navigation */}
-
-
-
+
- {/* Tab Content */}
{activeTab === 'templates' && (
)}
@@ -398,419 +86,3 @@ export default function EmailTemplatesPage() {
)
}
-
-// =============================================================================
-// TAB COMPONENTS
-// =============================================================================
-
-function TemplatesTab({
- templates, loading, selectedCategory, onCategoryChange, onEdit, onInitialize,
-}: {
- templates: EmailTemplate[]
- loading: boolean
- selectedCategory: string | null
- onCategoryChange: (cat: string | null) => void
- onEdit: (t: EmailTemplate) => void
- onInitialize: () => void
-}) {
- return (
-
- {/* Category Pills */}
-
-
- {Object.entries(CATEGORIES).map(([key, cat]) => (
-
- ))}
-
-
- {loading ? (
-
Lade Templates...
- ) : templates.length === 0 ? (
-
-
Keine Templates vorhanden.
-
-
- ) : (
-
- {templates.map(t => (
- onEdit(t)} />
- ))}
-
- )}
-
- )
-}
-
-function TemplateCard({ template, onEdit }: { template: EmailTemplate; onEdit: () => void }) {
- const cat = CATEGORIES[template.category] || CATEGORIES.general
- const version = template.latest_version
- const status = version ? (STATUS_BADGE[version.status] || STATUS_BADGE.draft) : STATUS_BADGE.draft
-
- return (
-
-
-
-
{template.name}
-
- {cat.label}
-
-
-
{status.label}
-
- {template.description && (
-
{template.description}
- )}
-
- {(template.variables || []).slice(0, 4).map(v => (
-
- {`{{${v}}}`}
-
- ))}
- {(template.variables || []).length > 4 && (
- +{template.variables.length - 4}
- )}
-
- {version && (
-
- v{version.version} · {version.created_at ? new Date(version.created_at).toLocaleDateString('de-DE') : ''}
-
- )}
-
- )
-}
-
-function EditorTab({
- template, version, subject, html, previewHtml, saving,
- onSubjectChange, onHtmlChange, onSave, onPublish, onPreview, onBack,
-}: {
- template: EmailTemplate | null
- version: TemplateVersion | null
- subject: string
- html: string
- previewHtml: string | null
- saving: boolean
- onSubjectChange: (v: string) => void
- onHtmlChange: (v: string) => void
- onSave: () => void
- onPublish: () => void
- onPreview: () => void
- onBack: () => void
-}) {
- if (!template) {
- return (
-
- Waehlen Sie ein Template aus der Liste.
-
-
-
- )
- }
-
- const cat = CATEGORIES[template.category] || CATEGORIES.general
-
- return (
-
-
-
-
-
{template.name}
- {cat.label}
- {version && (
-
- {(STATUS_BADGE[version.status] || STATUS_BADGE.draft).label}
-
- )}
-
-
-
- {version && version.status !== 'published' && (
-
- )}
- {version && (
-
- )}
-
-
-
- {/* Variables */}
-
- Variablen:
- {(template.variables || []).map(v => (
-
- ))}
-
-
- {/* Split View */}
-
- {/* Editor */}
-
-
-
- onSubjectChange(e.target.value)}
- className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent"
- placeholder="E-Mail Betreff..."
- />
-
-
-
-
- {/* Preview */}
-
-
-
- {previewHtml ? (
-
- ) : (
-
- )}
-
-
-
-
- )
-}
-
-function SettingsTab({
- settings, saving, onChange, onSave,
-}: {
- settings: Settings
- saving: boolean
- onChange: (s: Settings) => void
- onSave: () => void
-}) {
- const update = (field: keyof Settings, value: string) => {
- onChange({ ...settings, [field]: value })
- }
-
- return (
-
-
E-Mail-Einstellungen
-
-
-
-
-
-
- update('reply_to', e.target.value)}
- className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm"
- placeholder="optional"
- />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- )
-}
-
-function LogsTab({ logs, total }: { logs: SendLog[]; total: number }) {
- return (
-
-
E-Mail-Verlauf ({total})
-
- {logs.length === 0 ? (
-
Noch keine E-Mails gesendet.
- ) : (
-
-
-
-
- | Typ |
- Empfaenger |
- Betreff |
- Status |
- Datum |
-
-
-
- {logs.map(log => (
-
- | {log.template_type} |
- {log.recipient} |
- {log.subject} |
-
-
- {log.status}
-
- |
-
- {log.sent_at ? new Date(log.sent_at).toLocaleString('de-DE') : '-'}
- |
-
- ))}
-
-
-
- )}
-
- )
-}