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' 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({ export function VersionsTab({
loading, loading, documents, versions, selectedDocument, setSelectedDocument,
documents, onSubmitReview, onApprove, onReject, onPublish,
versions,
selectedDocument,
setSelectedDocument,
}: { }: {
loading: boolean loading: boolean
documents: Document[] documents: Document[]
versions: Version[] versions: Version[]
selectedDocument: string selectedDocument: string
setSelectedDocument: (id: string) => void setSelectedDocument: (id: string) => void
onSubmitReview?: (versionId: string) => void
onApprove?: (versionId: string) => void
onReject?: (versionId: string, comment: string) => void
onPublish?: (versionId: string) => void
}) { }) {
return ( return (
<div className="p-6"> <div className="p-6">
@@ -27,36 +37,24 @@ export function VersionsTab({
> >
<option value="">Dokument auswaehlen...</option> <option value="">Dokument auswaehlen...</option>
{documents.map((doc) => ( {documents.map((doc) => (
<option key={doc.id} value={doc.id}> <option key={doc.id} value={doc.id}>{doc.name}</option>
{doc.name}
</option>
))} ))}
</select> </select>
</div> </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> </div>
{!selectedDocument ? ( {!selectedDocument ? (
<div className="text-center py-12 text-slate-500"> <div className="text-center py-12 text-slate-500">Bitte waehlen Sie ein Dokument aus</div>
Bitte waehlen Sie ein Dokument aus
</div>
) : loading ? ( ) : loading ? (
<div className="text-center py-12 text-slate-500">Lade Versionen...</div> <div className="text-center py-12 text-slate-500">Lade Versionen...</div>
) : versions.length === 0 ? ( ) : versions.length === 0 ? (
<div className="text-center py-12 text-slate-500"> <div className="text-center py-12 text-slate-500">Keine Versionen vorhanden</div>
Keine Versionen vorhanden
</div>
) : ( ) : (
<div className="space-y-4"> <div className="space-y-4">
{versions.map((version) => ( {versions.map((version) => {
<div const style = STATUS_STYLES[version.status] || STATUS_STYLES.draft
key={version.id} return (
className="border border-slate-200 rounded-lg p-4 hover:border-purple-300 transition-colors" <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 className="flex items-start justify-between">
<div> <div>
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
@@ -64,36 +62,44 @@ export function VersionsTab({
<span className="px-2 py-0.5 bg-slate-100 text-slate-600 rounded text-xs"> <span className="px-2 py-0.5 bg-slate-100 text-slate-600 rounded text-xs">
{version.language.toUpperCase()} {version.language.toUpperCase()}
</span> </span>
<span <span className={`px-2 py-0.5 rounded text-xs ${style.color}`}>{style.label}</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>
</div> </div>
<h3 className="text-slate-700">{version.title}</h3> <h3 className="text-slate-700">{version.title}</h3>
<p className="text-sm text-slate-500 mt-1"> <p className="text-sm text-slate-500 mt-1">
Erstellt: {new Date(version.created_at).toLocaleDateString('de-DE')} Erstellt: {new Date(version.created_at).toLocaleDateString('de-DE')}
{version.published_at && ` | Publiziert: ${new Date(version.published_at).toLocaleDateString('de-DE')}`}
</p> </p>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2 flex-wrap justify-end">
<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"> {version.status === 'draft' && onSubmitReview && (
Bearbeiten <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> </button>
{version.status === 'draft' && ( )}
<button className="px-3 py-1.5 text-sm text-white bg-green-600 hover:bg-green-700 rounded-lg"> {version.status === 'review' && onApprove && (
Veroeffentlichen <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> </button>
)} )}
</div> </div>
</div> </div>
</div> </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)) 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 { return {
documents, versions, loading, error, setError, documents, versions, loading, error, setError,
consentStats, dsrCounts, dsrOverview, consentStats, dsrCounts, dsrOverview,
@@ -286,6 +325,7 @@ export function useConsentData(activeTab: Tab, selectedDocument: string) {
savingTemplateId, savingProcessId, savingTemplateId, savingProcessId,
saveApiEmailTemplate, saveApiGdprProcess, saveApiEmailTemplate, saveApiGdprProcess,
loadApiEmailTemplates, loadApiEmailTemplates,
submitVersionForReview, approveVersion, rejectVersion, publishVersion,
authToken, setAuthToken, authToken, setAuthToken,
} }
} }

View File

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