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:
@@ -15,11 +15,16 @@ interface EditorTabProps {
|
||||
onPublish: () => void
|
||||
onPreview: () => void
|
||||
onBack: () => void
|
||||
onSubmitForReview?: () => void
|
||||
onApprove?: (comment?: string) => void
|
||||
onReject?: (comment: string) => void
|
||||
onSendTest?: (email: string) => void
|
||||
}
|
||||
|
||||
export function EditorTab({
|
||||
template, version, subject, html, previewHtml, saving,
|
||||
onSubjectChange, onHtmlChange, onSave, onPublish, onPreview, onBack,
|
||||
onSubmitForReview, onApprove, onReject, onSendTest,
|
||||
}: EditorTabProps) {
|
||||
if (!template) {
|
||||
return (
|
||||
@@ -46,30 +51,56 @@ export function EditorTab({
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<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"
|
||||
>
|
||||
{saving ? 'Speichern...' : 'Version speichern'}
|
||||
</button>
|
||||
{version && version.status !== 'published' && (
|
||||
<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"
|
||||
>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{/* Save — always available for draft/review */}
|
||||
{(!version || version.status === 'draft' || version.status === 'review') && (
|
||||
<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">
|
||||
{saving ? 'Speichern...' : 'Version speichern'}
|
||||
</button>
|
||||
)}
|
||||
{/* Submit for Review — only for draft */}
|
||||
{version && version.status === 'draft' && onSubmitForReview && (
|
||||
<button onClick={onSubmitForReview} disabled={saving}
|
||||
className="px-3 py-1.5 bg-yellow-500 text-white rounded-lg text-sm hover:bg-yellow-600 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
|
||||
</button>
|
||||
)}
|
||||
{/* Preview + Test — always when version exists */}
|
||||
{version && (
|
||||
<button
|
||||
onClick={onPreview}
|
||||
className="px-3 py-1.5 border border-gray-300 text-gray-700 rounded-lg text-sm hover:bg-gray-50"
|
||||
>
|
||||
Vorschau
|
||||
</button>
|
||||
<>
|
||||
<button onClick={onPreview}
|
||||
className="px-3 py-1.5 border border-gray-300 text-gray-700 rounded-lg text-sm hover:bg-gray-50">
|
||||
Vorschau
|
||||
</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>
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
SendLog,
|
||||
Settings,
|
||||
TabId,
|
||||
TemplateApproval,
|
||||
TemplateType,
|
||||
TemplateVersion,
|
||||
getHeaders,
|
||||
@@ -194,6 +195,72 @@ export function useEmailTemplates(activeTab: TabId) {
|
||||
}
|
||||
}, [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 () => {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/initialize`, {
|
||||
@@ -222,6 +289,8 @@ export function useEmailTemplates(activeTab: TabId) {
|
||||
setSettingsForm,
|
||||
// Actions
|
||||
openEditor, saveVersion, publishVersion, loadPreview,
|
||||
submitForReview, approveVersion, rejectVersion,
|
||||
sendTestEmail, loadApprovalHistory,
|
||||
saveSettings2, initializeDefaults,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,16 @@ export interface SendLog {
|
||||
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 {
|
||||
sender_name: string
|
||||
sender_email: string
|
||||
|
||||
@@ -22,6 +22,8 @@ export default function EmailTemplatesPage() {
|
||||
setEditorSubject, setEditorHtml,
|
||||
setSettingsForm,
|
||||
openEditor, saveVersion, publishVersion, loadPreview,
|
||||
submitForReview, approveVersion, rejectVersion,
|
||||
sendTestEmail, loadApprovalHistory,
|
||||
saveSettings2, initializeDefaults,
|
||||
} = useEmailTemplates(activeTab)
|
||||
|
||||
@@ -68,6 +70,10 @@ export default function EmailTemplatesPage() {
|
||||
onPublish={publishVersion}
|
||||
onPreview={loadPreview}
|
||||
onBack={() => setActiveTab('templates')}
|
||||
onSubmitForReview={submitForReview}
|
||||
onApprove={approveVersion}
|
||||
onReject={rejectVersion}
|
||||
onSendTest={sendTestEmail}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user