feat: Email Template Approval Workflow im Frontend aktivieren

Backend-Endpoints existierten bereits (submit/approve/reject/publish),
wurden aber vom Frontend nicht genutzt. Jetzt vollstaendiger Workflow:

- Submit for Review: Entwurf → Pruefung einreichen
- Approve/Reject: DSB kann genehmigen oder mit Begruendung ablehnen
- Publish: Genehmigte Version veroeffentlichen
- Test senden: Test-E-Mail an beliebige Adresse
- Approval History: Genehmigungshistorie abrufbar
- Status-Badges: draft/review/approved/published mit passenden Buttons

Alle Buttons sind kontextabhaengig — nur sichtbar wenn der Status passt.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-04-27 23:42:26 +02:00
parent 918a9d8092
commit 8e0645481a
4 changed files with 136 additions and 20 deletions

View File

@@ -15,11 +15,16 @@ interface EditorTabProps {
onPublish: () => void onPublish: () => void
onPreview: () => void onPreview: () => void
onBack: () => void onBack: () => void
onSubmitForReview?: () => void
onApprove?: (comment?: string) => void
onReject?: (comment: string) => void
onSendTest?: (email: string) => void
} }
export function EditorTab({ export function EditorTab({
template, version, subject, html, previewHtml, saving, template, version, subject, html, previewHtml, saving,
onSubjectChange, onHtmlChange, onSave, onPublish, onPreview, onBack, onSubjectChange, onHtmlChange, onSave, onPublish, onPreview, onBack,
onSubmitForReview, onApprove, onReject, onSendTest,
}: EditorTabProps) { }: EditorTabProps) {
if (!template) { if (!template) {
return ( return (
@@ -46,30 +51,56 @@ export function EditorTab({
</span> </span>
)} )}
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2 flex-wrap">
<button {/* Save — always available for draft/review */}
onClick={onSave} {(!version || version.status === 'draft' || version.status === 'review') && (
disabled={saving} <button onClick={onSave} disabled={saving}
className="px-3 py-1.5 bg-purple-600 text-white rounded-lg text-sm hover:bg-purple-700 disabled:opacity-50" className="px-3 py-1.5 bg-purple-600 text-white rounded-lg text-sm hover:bg-purple-700 disabled:opacity-50">
> {saving ? 'Speichern...' : 'Version speichern'}
{saving ? 'Speichern...' : 'Version speichern'} </button>
</button> )}
{version && version.status !== 'published' && ( {/* Submit for Review — only for draft */}
<button {version && version.status === 'draft' && onSubmitForReview && (
onClick={onPublish} <button onClick={onSubmitForReview} disabled={saving}
disabled={saving} className="px-3 py-1.5 bg-yellow-500 text-white rounded-lg text-sm hover:bg-yellow-600 disabled:opacity-50">
className="px-3 py-1.5 bg-green-600 text-white rounded-lg text-sm hover:bg-green-700 disabled:opacity-50" Zur Pruefung einreichen
> </button>
)}
{/* Approve — only for review status (DSB) */}
{version && version.status === 'review' && onApprove && (
<button onClick={() => onApprove()} disabled={saving}
className="px-3 py-1.5 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700 disabled:opacity-50">
Genehmigen
</button>
)}
{/* Reject — only for review status (DSB) */}
{version && version.status === 'review' && onReject && (
<button onClick={() => { const c = prompt('Ablehnungsgrund:'); if (c) onReject(c) }} disabled={saving}
className="px-3 py-1.5 bg-red-500 text-white rounded-lg text-sm hover:bg-red-600 disabled:opacity-50">
Ablehnen
</button>
)}
{/* Publish — only for approved */}
{version && version.status === 'approved' && (
<button onClick={onPublish} disabled={saving}
className="px-3 py-1.5 bg-green-600 text-white rounded-lg text-sm hover:bg-green-700 disabled:opacity-50">
Publizieren Publizieren
</button> </button>
)} )}
{/* Preview + Test — always when version exists */}
{version && ( {version && (
<button <>
onClick={onPreview} <button onClick={onPreview}
className="px-3 py-1.5 border border-gray-300 text-gray-700 rounded-lg text-sm hover:bg-gray-50" className="px-3 py-1.5 border border-gray-300 text-gray-700 rounded-lg text-sm hover:bg-gray-50">
> Vorschau
Vorschau </button>
</button> {onSendTest && (
<button onClick={() => { const e = prompt('Test-E-Mail an:'); if (e) onSendTest(e) }}
className="px-3 py-1.5 border border-blue-300 text-blue-700 rounded-lg text-sm hover:bg-blue-50">
Test senden
</button>
)}
</>
)} )}
</div> </div>
</div> </div>

View File

@@ -7,6 +7,7 @@ import {
SendLog, SendLog,
Settings, Settings,
TabId, TabId,
TemplateApproval,
TemplateType, TemplateType,
TemplateVersion, TemplateVersion,
getHeaders, getHeaders,
@@ -194,6 +195,72 @@ export function useEmailTemplates(activeTab: TabId) {
} }
}, [settingsForm]) }, [settingsForm])
// Workflow actions
const submitForReview = useCallback(async () => {
if (!editorVersion) return
setSaving(true)
try {
const res = await fetch(`${API_BASE}/versions/${editorVersion.id}/submit`, {
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 approveVersion = useCallback(async (comment?: string) => {
if (!editorVersion) return
setSaving(true)
try {
const res = await fetch(`${API_BASE}/versions/${editorVersion.id}/approve`, {
method: 'POST', headers: getHeaders(),
body: JSON.stringify({ comment: comment || '' }),
})
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 rejectVersion = useCallback(async (comment: string) => {
if (!editorVersion) return
setSaving(true)
try {
const res = await fetch(`${API_BASE}/versions/${editorVersion.id}/reject`, {
method: 'POST', headers: getHeaders(),
body: JSON.stringify({ comment }),
})
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 sendTestEmail = useCallback(async (recipientEmail: string) => {
if (!editorVersion) return
try {
const res = await fetch(`${API_BASE}/versions/${editorVersion.id}/send-test`, {
method: 'POST', headers: getHeaders(),
body: JSON.stringify({ recipient: recipientEmail }),
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return await res.json()
} catch (e: any) { setError(e.message) }
}, [editorVersion])
const loadApprovalHistory = useCallback(async (versionId: string): Promise<TemplateApproval[]> => {
try {
const res = await fetch(`${API_BASE}/versions/${versionId}/approvals`, { headers: getHeaders() })
if (!res.ok) return []
const data = await res.json()
return Array.isArray(data) ? data : data.approvals || []
} catch { return [] }
}, [])
const initializeDefaults = useCallback(async () => { const initializeDefaults = useCallback(async () => {
try { try {
const res = await fetch(`${API_BASE}/initialize`, { const res = await fetch(`${API_BASE}/initialize`, {
@@ -222,6 +289,8 @@ export function useEmailTemplates(activeTab: TabId) {
setSettingsForm, setSettingsForm,
// Actions // Actions
openEditor, saveVersion, publishVersion, loadPreview, openEditor, saveVersion, publishVersion, loadPreview,
submitForReview, approveVersion, rejectVersion,
sendTestEmail, loadApprovalHistory,
saveSettings2, initializeDefaults, saveSettings2, initializeDefaults,
} }
} }

View File

@@ -42,6 +42,16 @@ export interface SendLog {
sent_at: string | null sent_at: string | null
} }
export interface TemplateApproval {
id: string
version_id: string
action: string // submitted, approved, rejected
actor_id: string
actor_name?: string
comment?: string
created_at: string
}
export interface Settings { export interface Settings {
sender_name: string sender_name: string
sender_email: string sender_email: string

View File

@@ -22,6 +22,8 @@ export default function EmailTemplatesPage() {
setEditorSubject, setEditorHtml, setEditorSubject, setEditorHtml,
setSettingsForm, setSettingsForm,
openEditor, saveVersion, publishVersion, loadPreview, openEditor, saveVersion, publishVersion, loadPreview,
submitForReview, approveVersion, rejectVersion,
sendTestEmail, loadApprovalHistory,
saveSettings2, initializeDefaults, saveSettings2, initializeDefaults,
} = useEmailTemplates(activeTab) } = useEmailTemplates(activeTab)
@@ -68,6 +70,10 @@ export default function EmailTemplatesPage() {
onPublish={publishVersion} onPublish={publishVersion}
onPreview={loadPreview} onPreview={loadPreview}
onBack={() => setActiveTab('templates')} onBack={() => setActiveTab('templates')}
onSubmitForReview={submitForReview}
onApprove={approveVersion}
onReject={rejectVersion}
onSendTest={sendTestEmail}
/> />
)} )}