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:
@@ -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,73 +37,69 @@ 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">
|
<span className="font-semibold text-slate-900">v{version.version}</span>
|
||||||
<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">
|
||||||
<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 className={`px-2 py-0.5 rounded text-xs ${style.color}`}>{style.label}</span>
|
||||||
<span
|
</div>
|
||||||
className={`px-2 py-0.5 rounded text-xs ${
|
<h3 className="text-slate-700">{version.title}</h3>
|
||||||
version.status === 'published'
|
<p className="text-sm text-slate-500 mt-1">
|
||||||
? 'bg-green-100 text-green-700'
|
Erstellt: {new Date(version.created_at).toLocaleDateString('de-DE')}
|
||||||
: version.status === 'draft'
|
{version.published_at && ` | Publiziert: ${new Date(version.published_at).toLocaleDateString('de-DE')}`}
|
||||||
? 'bg-yellow-100 text-yellow-700'
|
</p>
|
||||||
: 'bg-slate-100 text-slate-600'
|
</div>
|
||||||
}`}
|
<div className="flex gap-2 flex-wrap justify-end">
|
||||||
>
|
{version.status === 'draft' && onSubmitReview && (
|
||||||
{version.status}
|
<button onClick={() => onSubmitReview(version.id)}
|
||||||
</span>
|
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>
|
</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>
|
)
|
||||||
))}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user