feat: Consent Document Approval Workflow im Frontend aktivieren

VersionsTab zeigt jetzt kontextabhaengige Workflow-Buttons:
- Entwurf → "Zur Pruefung" (Submit for Review)
- In Pruefung → "Genehmigen" / "Ablehnen" (Approve/Reject)
- Genehmigt → "Publizieren" (Publish)

Backend-Endpoints (legal_document_routes.py) existierten bereits,
wurden aber vom Frontend nicht genutzt. Status-Badges erweitert:
draft, review, approved, published, archived, rejected.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-04-27 23:52:40 +02:00
parent 8e0645481a
commit 7dccdf4695
3 changed files with 109 additions and 58 deletions

View File

@@ -2,18 +2,28 @@
import type { Document, Version } from '../_types'
const STATUS_STYLES: Record<string, { label: string; color: string }> = {
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' },
archived: { label: 'Archiviert', color: 'bg-gray-100 text-gray-400' },
rejected: { label: 'Abgelehnt', color: 'bg-red-100 text-red-700' },
}
export function VersionsTab({
loading,
documents,
versions,
selectedDocument,
setSelectedDocument,
loading, documents, versions, selectedDocument, setSelectedDocument,
onSubmitReview, onApprove, onReject, onPublish,
}: {
loading: boolean
documents: Document[]
versions: Version[]
selectedDocument: string
setSelectedDocument: (id: string) => void
onSubmitReview?: (versionId: string) => void
onApprove?: (versionId: string) => void
onReject?: (versionId: string, comment: string) => void
onPublish?: (versionId: string) => void
}) {
return (
<div className="p-6">
@@ -27,73 +37,69 @@ export function VersionsTab({
>
<option value="">Dokument auswaehlen...</option>
{documents.map((doc) => (
<option key={doc.id} value={doc.id}>
{doc.name}
</option>
<option key={doc.id} value={doc.id}>{doc.name}</option>
))}
</select>
</div>
{selectedDocument && (
<button className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors text-sm font-medium">
+ Neue Version
</button>
)}
</div>
{!selectedDocument ? (
<div className="text-center py-12 text-slate-500">
Bitte waehlen Sie ein Dokument aus
</div>
<div className="text-center py-12 text-slate-500">Bitte waehlen Sie ein Dokument aus</div>
) : loading ? (
<div className="text-center py-12 text-slate-500">Lade Versionen...</div>
) : versions.length === 0 ? (
<div className="text-center py-12 text-slate-500">
Keine Versionen vorhanden
</div>
<div className="text-center py-12 text-slate-500">Keine Versionen vorhanden</div>
) : (
<div className="space-y-4">
{versions.map((version) => (
<div
key={version.id}
className="border border-slate-200 rounded-lg p-4 hover:border-purple-300 transition-colors"
>
<div className="flex items-start justify-between">
<div>
<div className="flex items-center gap-2 mb-1">
<span className="font-semibold text-slate-900">v{version.version}</span>
<span className="px-2 py-0.5 bg-slate-100 text-slate-600 rounded text-xs">
{version.language.toUpperCase()}
</span>
<span
className={`px-2 py-0.5 rounded text-xs ${
version.status === 'published'
? 'bg-green-100 text-green-700'
: version.status === 'draft'
? 'bg-yellow-100 text-yellow-700'
: 'bg-slate-100 text-slate-600'
}`}
>
{version.status}
</span>
{versions.map((version) => {
const style = STATUS_STYLES[version.status] || STATUS_STYLES.draft
return (
<div key={version.id} className="border border-slate-200 rounded-lg p-4 hover:border-purple-300 transition-colors">
<div className="flex items-start justify-between">
<div>
<div className="flex items-center gap-2 mb-1">
<span className="font-semibold text-slate-900">v{version.version}</span>
<span className="px-2 py-0.5 bg-slate-100 text-slate-600 rounded text-xs">
{version.language.toUpperCase()}
</span>
<span className={`px-2 py-0.5 rounded text-xs ${style.color}`}>{style.label}</span>
</div>
<h3 className="text-slate-700">{version.title}</h3>
<p className="text-sm text-slate-500 mt-1">
Erstellt: {new Date(version.created_at).toLocaleDateString('de-DE')}
{version.published_at && ` | Publiziert: ${new Date(version.published_at).toLocaleDateString('de-DE')}`}
</p>
</div>
<div className="flex gap-2 flex-wrap justify-end">
{version.status === 'draft' && onSubmitReview && (
<button onClick={() => onSubmitReview(version.id)}
className="px-3 py-1.5 text-sm text-white bg-yellow-500 hover:bg-yellow-600 rounded-lg">
Zur Pruefung
</button>
)}
{version.status === 'review' && onApprove && (
<button onClick={() => onApprove(version.id)}
className="px-3 py-1.5 text-sm text-white bg-blue-600 hover:bg-blue-700 rounded-lg">
Genehmigen
</button>
)}
{version.status === 'review' && onReject && (
<button onClick={() => { const c = prompt('Ablehnungsgrund:'); if (c) onReject(version.id, c) }}
className="px-3 py-1.5 text-sm text-white bg-red-500 hover:bg-red-600 rounded-lg">
Ablehnen
</button>
)}
{version.status === 'approved' && onPublish && (
<button onClick={() => onPublish(version.id)}
className="px-3 py-1.5 text-sm text-white bg-green-600 hover:bg-green-700 rounded-lg">
Publizieren
</button>
)}
</div>
<h3 className="text-slate-700">{version.title}</h3>
<p className="text-sm text-slate-500 mt-1">
Erstellt: {new Date(version.created_at).toLocaleDateString('de-DE')}
</p>
</div>
<div className="flex gap-2">
<button className="px-3 py-1.5 text-sm text-slate-600 hover:text-slate-900 border border-slate-300 rounded-lg hover:border-slate-400">
Bearbeiten
</button>
{version.status === 'draft' && (
<button className="px-3 py-1.5 text-sm text-white bg-green-600 hover:bg-green-700 rounded-lg">
Veroeffentlichen
</button>
)}
</div>
</div>
</div>
))}
)
})}
</div>
)}
</div>

View File

@@ -277,6 +277,45 @@ export function useConsentData(activeTab: Tab, selectedDocument: string) {
localStorage.setItem('sdk-email-templates', JSON.stringify(updated))
}
// Document version workflow actions (via admin consent proxy → legal-documents backend)
async function submitVersionForReview(versionId: string) {
try {
const res = await fetch(`${API_BASE}/versions/${versionId}/submit-review`, {
method: 'POST', headers: authToken ? { 'Authorization': `Bearer ${authToken}` } : {},
})
if (res.ok && selectedDocument) await loadVersions(selectedDocument)
} catch (err) { console.error('Submit failed:', err) }
}
async function approveVersion(versionId: string) {
try {
const res = await fetch(`${API_BASE}/versions/${versionId}/approve`, {
method: 'POST', headers: authToken ? { 'Authorization': `Bearer ${authToken}` } : {},
})
if (res.ok && selectedDocument) await loadVersions(selectedDocument)
} catch (err) { console.error('Approve failed:', err) }
}
async function rejectVersion(versionId: string, comment: string) {
try {
const res = await fetch(`${API_BASE}/versions/${versionId}/reject`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...(authToken ? { 'Authorization': `Bearer ${authToken}` } : {}) },
body: JSON.stringify({ comment }),
})
if (res.ok && selectedDocument) await loadVersions(selectedDocument)
} catch (err) { console.error('Reject failed:', err) }
}
async function publishVersion(versionId: string) {
try {
const res = await fetch(`${API_BASE}/versions/${versionId}/publish`, {
method: 'POST', headers: authToken ? { 'Authorization': `Bearer ${authToken}` } : {},
})
if (res.ok) { if (selectedDocument) await loadVersions(selectedDocument); await loadDocuments() }
} catch (err) { console.error('Publish failed:', err) }
}
return {
documents, versions, loading, error, setError,
consentStats, dsrCounts, dsrOverview,
@@ -286,6 +325,7 @@ export function useConsentData(activeTab: Tab, selectedDocument: string) {
savingTemplateId, savingProcessId,
saveApiEmailTemplate, saveApiGdprProcess,
loadApiEmailTemplates,
submitVersionForReview, approveVersion, rejectVersion, publishVersion,
authToken, setAuthToken,
}
}

View File

@@ -45,6 +45,7 @@ export default function ConsentManagementPage() {
savingTemplateId, savingProcessId,
saveApiEmailTemplate, saveApiGdprProcess,
loadApiEmailTemplates,
submitVersionForReview, approveVersion, rejectVersion, publishVersion,
authToken, setAuthToken,
} = useConsentData(activeTab, selectedDocument)
@@ -128,6 +129,10 @@ export default function ConsentManagementPage() {
versions={versions}
selectedDocument={selectedDocument}
setSelectedDocument={setSelectedDocument}
onSubmitReview={submitVersionForReview}
onApprove={approveVersion}
onReject={rejectVersion}
onPublish={publishVersion}
/>
)}