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
|
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' && (
|
)}
|
||||||
<button
|
{/* Submit for Review — only for draft */}
|
||||||
onClick={onPublish}
|
{version && version.status === 'draft' && onSubmitForReview && (
|
||||||
disabled={saving}
|
<button onClick={onSubmitForReview} disabled={saving}
|
||||||
className="px-3 py-1.5 bg-green-600 text-white rounded-lg text-sm hover:bg-green-700 disabled:opacity-50"
|
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
|
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>
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user