diff --git a/admin-compliance/app/sdk/workflow/_components/ApprovalModal.tsx b/admin-compliance/app/sdk/workflow/_components/ApprovalModal.tsx
new file mode 100644
index 0000000..1c6caed
--- /dev/null
+++ b/admin-compliance/app/sdk/workflow/_components/ApprovalModal.tsx
@@ -0,0 +1,55 @@
+'use client'
+
+interface ApprovalModalProps {
+ mode: 'approve' | 'reject'
+ approvalComment: string
+ onCommentChange: (comment: string) => void
+ onCancel: () => void
+ onConfirm: () => void
+ saving: boolean
+}
+
+export default function ApprovalModal({
+ mode,
+ approvalComment,
+ onCommentChange,
+ onCancel,
+ onConfirm,
+ saving,
+}: ApprovalModalProps) {
+ return (
+
+
+
+ {mode === 'approve' ? 'Version freigeben' : 'Version ablehnen'}
+
+
+
+ )
+}
diff --git a/admin-compliance/app/sdk/workflow/_components/CompareView.tsx b/admin-compliance/app/sdk/workflow/_components/CompareView.tsx
new file mode 100644
index 0000000..48936c7
--- /dev/null
+++ b/admin-compliance/app/sdk/workflow/_components/CompareView.tsx
@@ -0,0 +1,143 @@
+'use client'
+
+import { Version, STATUS_LABELS } from '../_types'
+
+interface CompareViewProps {
+ currentVersion: Version | null
+ draftVersion: Version | null
+ editedContent: string
+ onClose: () => void
+ onSaveDraft: () => void
+ onSubmitForReview: () => void
+ onShowApprovalModal: (mode: 'approve' | 'reject') => void
+ onPublishVersion: () => void
+}
+
+export default function CompareView({
+ currentVersion,
+ draftVersion,
+ editedContent,
+ onClose,
+ onSaveDraft,
+ onSubmitForReview,
+ onShowApprovalModal,
+ onPublishVersion,
+}: CompareViewProps) {
+ return (
+
+ {/* Header */}
+
+
+
Versionsvergleich
+
+ {currentVersion ? `v${currentVersion.version}` : 'Keine Version'}
+ vs
+ {draftVersion ? `v${draftVersion.version}` : 'Neue Version'}
+
+
+
+
+
+ {/* Compare Panels */}
+
+ {/* Left: Published */}
+
+
+ Veroeffentlichte Version
+ {currentVersion && (
+ v{currentVersion.version}
+ )}
+
+
+ {currentVersion ? (
+
+ ) : (
+
Keine veroeffentlichte Version
+ )}
+
+
+
+ {/* Right: Draft */}
+
+
+
+ {draftVersion ? 'Aenderungsversion' : 'Neue Version'}
+
+ {draftVersion && (
+
+ v{draftVersion.version} - {STATUS_LABELS[draftVersion.status].label}
+
+ )}
+
+
+
+
+
+ {/* Footer with Actions */}
+
+ {draftVersion?.status === 'draft' && (
+ <>
+
+
+ >
+ )}
+ {draftVersion?.status === 'review' && (
+ <>
+
+
+ >
+ )}
+ {draftVersion?.status === 'approved' && (
+
+ )}
+
+
+ )
+}
diff --git a/admin-compliance/app/sdk/workflow/_components/DocumentSelectorBar.tsx b/admin-compliance/app/sdk/workflow/_components/DocumentSelectorBar.tsx
new file mode 100644
index 0000000..605e018
--- /dev/null
+++ b/admin-compliance/app/sdk/workflow/_components/DocumentSelectorBar.tsx
@@ -0,0 +1,85 @@
+'use client'
+
+import { Document, Version, STATUS_LABELS, DOCUMENT_TYPES } from '../_types'
+
+interface DocumentSelectorBarProps {
+ documents: Document[]
+ selectedDocument: Document | null
+ onSelectDocument: (doc: Document | null) => void
+ currentVersion: Version | null
+ draftVersion: Version | null
+ onNewDocument: () => void
+ onCompareView: () => void
+ onToggleHistory: () => void
+}
+
+export default function DocumentSelectorBar({
+ documents,
+ selectedDocument,
+ onSelectDocument,
+ currentVersion,
+ draftVersion,
+ onNewDocument,
+ onCompareView,
+ onToggleHistory,
+}: DocumentSelectorBarProps) {
+ return (
+
+
+
+
+
+
+
+
+ {currentVersion && (
+
+ Aktuelle Version: v{currentVersion.version}
+
+ )}
+ {draftVersion && (
+
+ {STATUS_LABELS[draftVersion.status].label}: v{draftVersion.version}
+
+ )}
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/admin-compliance/app/sdk/workflow/_components/HistoryPanel.tsx b/admin-compliance/app/sdk/workflow/_components/HistoryPanel.tsx
new file mode 100644
index 0000000..be97a3a
--- /dev/null
+++ b/admin-compliance/app/sdk/workflow/_components/HistoryPanel.tsx
@@ -0,0 +1,73 @@
+'use client'
+
+import { Version, ApprovalHistoryItem, STATUS_LABELS } from '../_types'
+
+interface HistoryPanelProps {
+ approvalHistory: ApprovalHistoryItem[]
+ versions: Version[]
+ draftVersion: Version | null
+ currentVersion: Version | null
+}
+
+export default function HistoryPanel({
+ approvalHistory,
+ versions,
+ draftVersion,
+ currentVersion,
+}: HistoryPanelProps) {
+ return (
+
+
Genehmigungsverlauf
+ {approvalHistory.length > 0 ? (
+
+ {approvalHistory.map((item, idx) => (
+
+ {item.action}
+ {item.approver || 'System'}
+ {item.comment && (
+ "{item.comment}"
+ )}
+
+ {new Date(item.created_at).toLocaleString('de-DE')}
+
+
+ ))}
+
+ ) : (
+
Keine Genehmigungshistorie vorhanden.
+ )}
+
+
Alle Versionen
+
+ {versions.map((v) => (
+
+
+
+ v{v.version}
+
+ {STATUS_LABELS[v.status].label}
+
+ {v.title}
+
+
+ {new Date(v.created_at).toLocaleDateString('de-DE')}
+
+
+
+ ))}
+
+
+ )
+}
diff --git a/admin-compliance/app/sdk/workflow/_components/NewDocumentModal.tsx b/admin-compliance/app/sdk/workflow/_components/NewDocumentModal.tsx
new file mode 100644
index 0000000..1b2302a
--- /dev/null
+++ b/admin-compliance/app/sdk/workflow/_components/NewDocumentModal.tsx
@@ -0,0 +1,84 @@
+'use client'
+
+interface NewDocForm {
+ type: string
+ name: string
+ description: string
+}
+
+interface NewDocumentModalProps {
+ newDocForm: NewDocForm
+ onChange: (form: NewDocForm) => void
+ onClose: () => void
+ onCreate: () => void
+ creatingDoc: boolean
+}
+
+export default function NewDocumentModal({
+ newDocForm,
+ onChange,
+ onClose,
+ onCreate,
+ creatingDoc,
+}: NewDocumentModalProps) {
+ return (
+
+
+
+
Neues Dokument erstellen
+
+
+
+
+
+
+
+
+ onChange({ ...newDocForm, name: e.target.value })}
+ placeholder="z.B. Datenschutzerklärung Website"
+ className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
+ />
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/admin-compliance/app/sdk/workflow/_components/RichTextToolbar.tsx b/admin-compliance/app/sdk/workflow/_components/RichTextToolbar.tsx
new file mode 100644
index 0000000..1b2dd19
--- /dev/null
+++ b/admin-compliance/app/sdk/workflow/_components/RichTextToolbar.tsx
@@ -0,0 +1,101 @@
+'use client'
+
+import { RefObject } from 'react'
+
+interface RichTextToolbarProps {
+ fileInputRef: RefObject
+ uploading: boolean
+ onFormatDoc: (cmd: string, value?: string | null) => void
+ onFormatBlock: (tag: string) => void
+ onInsertLink: () => void
+ onWordUpload: (event: React.ChangeEvent) => void
+}
+
+export default function RichTextToolbar({
+ fileInputRef,
+ uploading,
+ onFormatDoc,
+ onFormatBlock,
+ onInsertLink,
+ onWordUpload,
+}: RichTextToolbarProps) {
+ return (
+
+
+ {/* Formatting */}
+
+
+
+
+
+
+ {/* Headings */}
+
+
+
+
+
+
+
+ {/* Lists */}
+
+
+
+
+
+ {/* Links */}
+
+
+ {/* Word Upload */}
+
+
+
+
+
+
+ )
+}
diff --git a/admin-compliance/app/sdk/workflow/_components/SplitViewEditor.tsx b/admin-compliance/app/sdk/workflow/_components/SplitViewEditor.tsx
new file mode 100644
index 0000000..011d5b6
--- /dev/null
+++ b/admin-compliance/app/sdk/workflow/_components/SplitViewEditor.tsx
@@ -0,0 +1,148 @@
+'use client'
+
+import { RefObject } from 'react'
+import { Version, STATUS_LABELS } from '../_types'
+
+interface SplitViewEditorProps {
+ leftPanelRef: RefObject
+ rightPanelRef: RefObject
+ editorRef: RefObject
+ currentVersion: Version | null
+ draftVersion: Version | null
+ isEditable: boolean
+ editedTitle: string
+ editedContent: string
+ onTitleChange: (title: string) => void
+ onUpdateEditorContent: () => void
+ onPaste: (e: React.ClipboardEvent) => void
+}
+
+export default function SplitViewEditor({
+ leftPanelRef,
+ rightPanelRef,
+ editorRef,
+ currentVersion,
+ draftVersion,
+ isEditable,
+ editedTitle,
+ editedContent,
+ onTitleChange,
+ onUpdateEditorContent,
+ onPaste,
+}: SplitViewEditorProps) {
+ return (
+
+ {/* Left: Current Published Version */}
+
+
+
+
Veroeffentlichte Version
+ {currentVersion && (
+
v{currentVersion.version}
+ )}
+
+
+ Nur Lesen
+
+
+
+ {currentVersion ? (
+ <>
+
+
+ >
+ ) : (
+
+ Keine veroeffentlichte Version vorhanden
+
+ )}
+
+
+
+ {/* Right: Draft/Edit Version */}
+
+
+
+
+ {draftVersion ? 'Aenderungsversion' : 'Neue Version'}
+
+ {draftVersion && (
+
+ v{draftVersion.version} - {STATUS_LABELS[draftVersion.status].label}
+
+ )}
+
+ {isEditable && (
+
+ Bearbeitbar
+
+ )}
+
+
+
onTitleChange(e.target.value)}
+ disabled={!isEditable}
+ placeholder="Titel der Version..."
+ className={`w-full px-3 py-2 mb-4 border rounded-lg ${
+ isEditable ? 'border-slate-300 bg-white' : 'border-slate-200 bg-slate-50 text-slate-700'
+ }`}
+ />
+
+ {isEditable ? (
+
+ ) : (
+
+ )}
+
+ {/* Character count */}
+
+ {(editorRef.current?.textContent || editedContent.replace(/<[^>]*>/g, '')).length} Zeichen
+
+
+
+
+ )
+}
diff --git a/admin-compliance/app/sdk/workflow/_components/WorkflowStatusBar.tsx b/admin-compliance/app/sdk/workflow/_components/WorkflowStatusBar.tsx
new file mode 100644
index 0000000..63e433f
--- /dev/null
+++ b/admin-compliance/app/sdk/workflow/_components/WorkflowStatusBar.tsx
@@ -0,0 +1,126 @@
+'use client'
+
+import { Version } from '../_types'
+
+interface WorkflowStatusBarProps {
+ draftVersion: Version | null
+ saving: boolean
+ onCreateNewDraft: () => void
+ onSaveDraft: () => void
+ onSubmitForReview: () => void
+ onShowApprovalModal: (mode: 'approve' | 'reject') => void
+ onPublishVersion: () => void
+}
+
+export default function WorkflowStatusBar({
+ draftVersion,
+ saving,
+ onCreateNewDraft,
+ onSaveDraft,
+ onSubmitForReview,
+ onShowApprovalModal,
+ onPublishVersion,
+}: WorkflowStatusBarProps) {
+ return (
+
+
+
+ {['draft', 'review', 'approved', 'published'].map((status, idx) => (
+
+ {idx > 0 &&
}
+
+
{idx + 1}
+
+ {status === 'draft' ? 'Entwurf' :
+ status === 'review' ? 'Pruefung' :
+ status === 'approved' ? 'Freigegeben' : 'Veroeffentlicht'}
+
+
+
+ ))}
+
+
+ {/* Action Buttons */}
+
+ {!draftVersion && (
+
+ )}
+
+ {draftVersion?.status === 'draft' && (
+ <>
+
+
+ >
+ )}
+
+ {draftVersion?.status === 'review' && (
+ <>
+
+
+ >
+ )}
+
+ {draftVersion?.status === 'approved' && (
+
+ )}
+
+ {draftVersion?.status === 'rejected' && (
+
+ Abgelehnt: {draftVersion.rejection_reason}
+
+
+ )}
+
+
+
+ )
+}
diff --git a/admin-compliance/app/sdk/workflow/_hooks/useRichTextEditor.ts b/admin-compliance/app/sdk/workflow/_hooks/useRichTextEditor.ts
new file mode 100644
index 0000000..6bb015c
--- /dev/null
+++ b/admin-compliance/app/sdk/workflow/_hooks/useRichTextEditor.ts
@@ -0,0 +1,131 @@
+'use client'
+
+import { useState, useRef, RefObject } from 'react'
+
+interface UseRichTextEditorResult {
+ editorRef: RefObject
+ fileInputRef: RefObject
+ uploading: boolean
+ uploadError: string | null
+ setUploadError: (e: string | null) => void
+ formatDoc: (cmd: string, value?: string | null) => void
+ formatBlock: (tag: string) => void
+ insertLink: () => void
+ updateEditorContent: () => void
+ handleWordUpload: (event: React.ChangeEvent) => Promise
+ handlePaste: (e: React.ClipboardEvent) => void
+}
+
+export function useRichTextEditor(
+ setEditedContent: (content: string) => void,
+): UseRichTextEditorResult {
+ const editorRef = useRef(null)
+ const fileInputRef = useRef(null)
+ const [uploading, setUploading] = useState(false)
+ const [uploadError, setUploadError] = useState(null)
+
+ const updateEditorContent = () => {
+ if (editorRef.current) {
+ setEditedContent(editorRef.current.innerHTML)
+ }
+ }
+
+ const formatDoc = (cmd: string, value: string | null = null) => {
+ if (editorRef.current) {
+ editorRef.current.focus()
+ document.execCommand(cmd, false, value || undefined)
+ updateEditorContent()
+ }
+ }
+
+ const formatBlock = (tag: string) => {
+ if (editorRef.current) {
+ editorRef.current.focus()
+ document.execCommand('formatBlock', false, `<${tag}>`)
+ updateEditorContent()
+ }
+ }
+
+ const insertLink = () => {
+ const url = prompt('Link-URL eingeben:', 'https://')
+ if (url && editorRef.current) {
+ editorRef.current.focus()
+ document.execCommand('createLink', false, url)
+ updateEditorContent()
+ }
+ }
+
+ const cleanWordHtml = (html: string): string => {
+ let cleaned = html
+ cleaned = cleaned.replace(/\s*mso-[^:]+:[^;]+;?/gi, '')
+ cleaned = cleaned.replace(/\s*style="[^"]*"/gi, '')
+ cleaned = cleaned.replace(/\s*class="[^"]*"/gi, '')
+ cleaned = cleaned.replace(/<\/o:p>/gi, '')
+ cleaned = cleaned.replace(/<\/?o:[^>]*>/gi, '')
+ cleaned = cleaned.replace(/<\/?w:[^>]*>/gi, '')
+ cleaned = cleaned.replace(/<\/?m:[^>]*>/gi, '')
+ cleaned = cleaned.replace(/]*>\s*<\/span>/gi, '')
+ return cleaned
+ }
+
+ const handleWordUpload = async (event: React.ChangeEvent) => {
+ const file = event.target.files?.[0]
+ if (!file) return
+
+ setUploading(true)
+
+ const formData = new FormData()
+ formData.append('file', file)
+
+ try {
+ const response = await fetch('/api/admin/consent/versions/upload-word', {
+ method: 'POST',
+ body: formData
+ })
+
+ if (response.ok) {
+ const data = await response.json()
+ if (editorRef.current) {
+ editorRef.current.innerHTML = data.html || 'Konvertierung fehlgeschlagen
'
+ setEditedContent(editorRef.current.innerHTML)
+ }
+ } else {
+ const errorData = await response.json().catch(() => ({}))
+ setUploadError('Fehler beim Importieren: ' + (errorData.detail || 'Unbekannter Fehler'))
+ }
+ } catch (e) {
+ setUploadError('Fehler beim Hochladen: ' + (e instanceof Error ? e.message : 'Unbekannter Fehler'))
+ } finally {
+ setUploading(false)
+ if (fileInputRef.current) {
+ fileInputRef.current.value = ''
+ }
+ }
+ }
+
+ const handlePaste = (e: React.ClipboardEvent) => {
+ const clipboardData = e.clipboardData
+ const html = clipboardData.getData('text/html')
+
+ if (html) {
+ e.preventDefault()
+ const cleanHtml = cleanWordHtml(html)
+ document.execCommand('insertHTML', false, cleanHtml)
+ updateEditorContent()
+ }
+ }
+
+ return {
+ editorRef,
+ fileInputRef,
+ uploading,
+ uploadError,
+ setUploadError,
+ formatDoc,
+ formatBlock,
+ insertLink,
+ updateEditorContent,
+ handleWordUpload,
+ handlePaste,
+ }
+}
diff --git a/admin-compliance/app/sdk/workflow/_hooks/useSyncScroll.ts b/admin-compliance/app/sdk/workflow/_hooks/useSyncScroll.ts
new file mode 100644
index 0000000..21456f5
--- /dev/null
+++ b/admin-compliance/app/sdk/workflow/_hooks/useSyncScroll.ts
@@ -0,0 +1,55 @@
+'use client'
+
+import { useCallback, useEffect, useRef, RefObject } from 'react'
+
+export function useSyncScroll(
+ leftPanelRef: RefObject,
+ rightPanelRef: RefObject,
+ deps: unknown[],
+) {
+ const isScrolling = useRef(false)
+
+ const setupSyncScroll = useCallback(() => {
+ const leftPanel = leftPanelRef.current
+ const rightPanel = rightPanelRef.current
+
+ if (!leftPanel || !rightPanel) return
+
+ const handleLeftScroll = () => {
+ if (isScrolling.current) return
+ isScrolling.current = true
+
+ const leftScrollPercent = leftPanel.scrollTop / (leftPanel.scrollHeight - leftPanel.clientHeight || 1)
+ const rightMaxScroll = rightPanel.scrollHeight - rightPanel.clientHeight
+ rightPanel.scrollTop = leftScrollPercent * rightMaxScroll
+
+ setTimeout(() => { isScrolling.current = false }, 10)
+ }
+
+ const handleRightScroll = () => {
+ if (isScrolling.current) return
+ isScrolling.current = true
+
+ const rightScrollPercent = rightPanel.scrollTop / (rightPanel.scrollHeight - rightPanel.clientHeight || 1)
+ const leftMaxScroll = leftPanel.scrollHeight - leftPanel.clientHeight
+ leftPanel.scrollTop = rightScrollPercent * leftMaxScroll
+
+ setTimeout(() => { isScrolling.current = false }, 10)
+ }
+
+ leftPanel.addEventListener('scroll', handleLeftScroll)
+ rightPanel.addEventListener('scroll', handleRightScroll)
+
+ return () => {
+ leftPanel.removeEventListener('scroll', handleLeftScroll)
+ rightPanel.removeEventListener('scroll', handleRightScroll)
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [])
+
+ useEffect(() => {
+ const cleanup = setupSyncScroll()
+ return cleanup
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [setupSyncScroll, ...deps])
+}
diff --git a/admin-compliance/app/sdk/workflow/_hooks/useWorkflowActions.ts b/admin-compliance/app/sdk/workflow/_hooks/useWorkflowActions.ts
new file mode 100644
index 0000000..46052b6
--- /dev/null
+++ b/admin-compliance/app/sdk/workflow/_hooks/useWorkflowActions.ts
@@ -0,0 +1,248 @@
+'use client'
+
+import { useState } from 'react'
+import { Document, Version, ApprovalHistoryItem } from '../_types'
+
+interface UseWorkflowActionsParams {
+ selectedDocument: Document | null
+ setDocuments: React.Dispatch>
+ setSelectedDocument: React.Dispatch>
+ draftVersion: Version | null
+ currentVersion: Version | null
+ versions: Version[]
+ editedTitle: string
+ editedContent: string
+ editedSummary: string
+ loadVersions: (docId: string) => Promise
+ setError: (msg: string | null) => void
+}
+
+export function useWorkflowActions(params: UseWorkflowActionsParams) {
+ const {
+ selectedDocument, setDocuments, setSelectedDocument,
+ draftVersion, currentVersion, versions,
+ editedTitle, editedContent, editedSummary,
+ loadVersions, setError,
+ } = params
+
+ const [saving, setSaving] = useState(false)
+ const [approvalComment, setApprovalComment] = useState('')
+ const [showApprovalModal, setShowApprovalModal] = useState<'approve' | 'reject' | null>(null)
+ const [approvalHistory, setApprovalHistory] = useState([])
+ const [showNewDocModal, setShowNewDocModal] = useState(false)
+ const [newDocForm, setNewDocForm] = useState({ type: 'privacy_policy', name: '', description: '' })
+ const [creatingDoc, setCreatingDoc] = useState(false)
+
+ const getNextVersionNumber = () => {
+ if (versions.length === 0) return '1.0'
+ const latest = versions[0]
+ const parts = latest.version.split('.')
+ const major = parseInt(parts[0]) || 1
+ const minor = parseInt(parts[1]) || 0
+ return `${major}.${minor + 1}`
+ }
+
+ const createNewDraft = async () => {
+ if (!selectedDocument) return
+ setSaving(true)
+ try {
+ const nextVersion = getNextVersionNumber()
+ const res = await fetch('/api/admin/consent/versions', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ document_id: selectedDocument.id,
+ version: nextVersion,
+ language: 'de',
+ title: editedTitle || currentVersion?.title || selectedDocument.name,
+ content: editedContent || currentVersion?.content || '',
+ summary: editedSummary || currentVersion?.summary || '',
+ }),
+ })
+
+ if (res.ok) {
+ await loadVersions(selectedDocument.id)
+ } else {
+ const err = await res.json()
+ setError(err.error || 'Fehler beim Erstellen des Entwurfs')
+ }
+ } catch {
+ setError('Fehler beim Erstellen des Entwurfs')
+ } finally {
+ setSaving(false)
+ }
+ }
+
+ const saveDraft = async () => {
+ if (!draftVersion || draftVersion.status !== 'draft') return
+ setSaving(true)
+ try {
+ const res = await fetch(`/api/admin/consent/versions/${draftVersion.id}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ title: editedTitle,
+ content: editedContent,
+ summary: editedSummary,
+ }),
+ })
+
+ if (res.ok) {
+ await loadVersions(selectedDocument!.id)
+ } else {
+ const err = await res.json()
+ setError(err.error || 'Fehler beim Speichern')
+ }
+ } catch {
+ setError('Fehler beim Speichern')
+ } finally {
+ setSaving(false)
+ }
+ }
+
+ const submitForReview = async () => {
+ if (!draftVersion) return
+ setSaving(true)
+ try {
+ const res = await fetch(`/api/admin/consent/versions/${draftVersion.id}/submit-review`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ })
+
+ if (res.ok) {
+ await loadVersions(selectedDocument!.id)
+ } else {
+ const err = await res.json()
+ setError(err.error || 'Fehler beim Einreichen')
+ }
+ } catch {
+ setError('Fehler beim Einreichen zur Pruefung')
+ } finally {
+ setSaving(false)
+ }
+ }
+
+ const approveVersion = async () => {
+ if (!draftVersion) return
+ setSaving(true)
+ try {
+ const res = await fetch(`/api/admin/consent/versions/${draftVersion.id}/approve`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ comment: approvalComment }),
+ })
+
+ if (res.ok) {
+ setShowApprovalModal(null)
+ setApprovalComment('')
+ await loadVersions(selectedDocument!.id)
+ } else {
+ const err = await res.json()
+ setError(err.error || 'Fehler bei der Freigabe')
+ }
+ } catch {
+ setError('Fehler bei der Freigabe')
+ } finally {
+ setSaving(false)
+ }
+ }
+
+ const rejectVersion = async () => {
+ if (!draftVersion) return
+ setSaving(true)
+ try {
+ const res = await fetch(`/api/admin/consent/versions/${draftVersion.id}/reject`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ reason: approvalComment }),
+ })
+
+ if (res.ok) {
+ setShowApprovalModal(null)
+ setApprovalComment('')
+ await loadVersions(selectedDocument!.id)
+ } else {
+ const err = await res.json()
+ setError(err.error || 'Fehler bei der Ablehnung')
+ }
+ } catch {
+ setError('Fehler bei der Ablehnung')
+ } finally {
+ setSaving(false)
+ }
+ }
+
+ const publishVersion = async () => {
+ if (!draftVersion || draftVersion.status !== 'approved') return
+ if (!confirm('Version wirklich veroeffentlichen? Die aktuelle Version wird archiviert.')) return
+
+ setSaving(true)
+ try {
+ const res = await fetch(`/api/admin/consent/versions/${draftVersion.id}/publish`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ })
+
+ if (res.ok) {
+ await loadVersions(selectedDocument!.id)
+ } else {
+ const err = await res.json()
+ setError(err.error || 'Fehler beim Veroeffentlichen')
+ }
+ } catch {
+ setError('Fehler beim Veroeffentlichen')
+ } finally {
+ setSaving(false)
+ }
+ }
+
+ const createDocument = async () => {
+ if (!newDocForm.name.trim()) return
+ setCreatingDoc(true)
+ try {
+ const res = await fetch('/api/admin/consent/documents', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(newDocForm),
+ })
+ if (res.ok) {
+ const newDoc: Document = await res.json()
+ setDocuments(prev => [newDoc, ...prev])
+ setSelectedDocument(newDoc)
+ setShowNewDocModal(false)
+ setNewDocForm({ type: 'privacy_policy', name: '', description: '' })
+ } else {
+ setError('Fehler beim Erstellen des Dokuments')
+ }
+ } catch {
+ setError('Verbindungsfehler beim Erstellen')
+ } finally {
+ setCreatingDoc(false)
+ }
+ }
+
+ const loadApprovalHistory = async (versionId: string) => {
+ try {
+ const res = await fetch(`/api/admin/consent/versions/${versionId}/approval-history`)
+ if (res.ok) {
+ const data = await res.json()
+ setApprovalHistory(data.approval_history || [])
+ }
+ } catch {
+ console.error('Failed to load approval history')
+ }
+ }
+
+ return {
+ saving,
+ approvalComment, setApprovalComment,
+ showApprovalModal, setShowApprovalModal,
+ approvalHistory,
+ showNewDocModal, setShowNewDocModal,
+ newDocForm, setNewDocForm,
+ creatingDoc,
+ createNewDraft, saveDraft, submitForReview,
+ approveVersion, rejectVersion, publishVersion,
+ createDocument, loadApprovalHistory,
+ }
+}
diff --git a/admin-compliance/app/sdk/workflow/_types.ts b/admin-compliance/app/sdk/workflow/_types.ts
new file mode 100644
index 0000000..d9c0e65
--- /dev/null
+++ b/admin-compliance/app/sdk/workflow/_types.ts
@@ -0,0 +1,50 @@
+export interface Document {
+ id: string
+ type: string
+ name: string
+ description: string
+ mandatory: boolean
+ created_at: string
+ updated_at: string
+}
+
+export interface Version {
+ id: string
+ document_id: string
+ version: string
+ language: string
+ title: string
+ content: string
+ summary?: string
+ status: 'draft' | 'review' | 'approved' | 'published' | 'archived' | 'rejected'
+ created_at: string
+ updated_at?: string
+ created_by?: string
+ approved_by?: string
+ approved_at?: string
+ rejection_reason?: string
+}
+
+export interface ApprovalHistoryItem {
+ action: string
+ approver: string
+ comment: string
+ created_at: string
+}
+
+export const STATUS_LABELS: Record = {
+ draft: { label: 'Entwurf', color: 'bg-yellow-100 text-yellow-700' },
+ review: { label: 'In Pruefung', color: 'bg-blue-100 text-blue-700' },
+ approved: { label: 'Freigegeben', color: 'bg-green-100 text-green-700' },
+ published: { label: 'Veroeffentlicht', color: 'bg-emerald-100 text-emerald-700' },
+ archived: { label: 'Archiviert', color: 'bg-slate-100 text-slate-700' },
+ rejected: { label: 'Abgelehnt', color: 'bg-red-100 text-red-700' },
+}
+
+export const DOCUMENT_TYPES: Record = {
+ terms: 'AGB',
+ privacy: 'Datenschutzerklaerung',
+ cookies: 'Cookie-Richtlinie',
+ community_guidelines: 'Community-Richtlinien',
+ imprint: 'Impressum',
+}
diff --git a/admin-compliance/app/sdk/workflow/page.tsx b/admin-compliance/app/sdk/workflow/page.tsx
index 1adb6a6..5661344 100644
--- a/admin-compliance/app/sdk/workflow/page.tsx
+++ b/admin-compliance/app/sdk/workflow/page.tsx
@@ -11,145 +11,47 @@
* - Rich text editor with formatting toolbar
*/
-import { useState, useEffect, useRef, useCallback } from 'react'
+import { useState, useEffect, useRef } from 'react'
import { useSDK } from '@/lib/sdk'
import StepHeader from '@/components/sdk/StepHeader/StepHeader'
-
-// Types
-interface Document {
- id: string
- type: string
- name: string
- description: string
- mandatory: boolean
- created_at: string
- updated_at: string
-}
-
-interface Version {
- id: string
- document_id: string
- version: string
- language: string
- title: string
- content: string
- summary?: string
- status: 'draft' | 'review' | 'approved' | 'published' | 'archived' | 'rejected'
- created_at: string
- updated_at?: string
- created_by?: string
- approved_by?: string
- approved_at?: string
- rejection_reason?: string
-}
-
-interface ApprovalHistoryItem {
- action: string
- approver: string
- comment: string
- created_at: string
-}
-
-const STATUS_LABELS: Record = {
- draft: { label: 'Entwurf', color: 'bg-yellow-100 text-yellow-700' },
- review: { label: 'In Pruefung', color: 'bg-blue-100 text-blue-700' },
- approved: { label: 'Freigegeben', color: 'bg-green-100 text-green-700' },
- published: { label: 'Veroeffentlicht', color: 'bg-emerald-100 text-emerald-700' },
- archived: { label: 'Archiviert', color: 'bg-slate-100 text-slate-700' },
- rejected: { label: 'Abgelehnt', color: 'bg-red-100 text-red-700' },
-}
-
-const DOCUMENT_TYPES: Record = {
- terms: 'AGB',
- privacy: 'Datenschutzerklaerung',
- cookies: 'Cookie-Richtlinie',
- community_guidelines: 'Community-Richtlinien',
- imprint: 'Impressum',
-}
+import { Document, Version } from './_types'
+import DocumentSelectorBar from './_components/DocumentSelectorBar'
+import WorkflowStatusBar from './_components/WorkflowStatusBar'
+import RichTextToolbar from './_components/RichTextToolbar'
+import SplitViewEditor from './_components/SplitViewEditor'
+import HistoryPanel from './_components/HistoryPanel'
+import CompareView from './_components/CompareView'
+import NewDocumentModal from './_components/NewDocumentModal'
+import ApprovalModal from './_components/ApprovalModal'
+import { useWorkflowActions } from './_hooks/useWorkflowActions'
+import { useRichTextEditor } from './_hooks/useRichTextEditor'
+import { useSyncScroll } from './_hooks/useSyncScroll'
export default function WorkflowPage() {
- const { state } = useSDK()
+ useSDK()
const [documents, setDocuments] = useState([])
const [versions, setVersions] = useState([])
const [selectedDocument, setSelectedDocument] = useState(null)
const [currentVersion, setCurrentVersion] = useState(null)
const [draftVersion, setDraftVersion] = useState(null)
const [loading, setLoading] = useState(true)
- const [saving, setSaving] = useState(false)
const [error, setError] = useState(null)
const [editedContent, setEditedContent] = useState('')
const [editedTitle, setEditedTitle] = useState('')
const [editedSummary, setEditedSummary] = useState('')
const [showHistory, setShowHistory] = useState(false)
- const [approvalHistory, setApprovalHistory] = useState([])
- const [approvalComment, setApprovalComment] = useState('')
- const [showApprovalModal, setShowApprovalModal] = useState<'approve' | 'reject' | null>(null)
const [showCompareView, setShowCompareView] = useState(false)
- const [uploading, setUploading] = useState(false)
- const [uploadError, setUploadError] = useState(null)
- const [showNewDocModal, setShowNewDocModal] = useState(false)
- const [newDocForm, setNewDocForm] = useState({ type: 'privacy_policy', name: '', description: '' })
- const [creatingDoc, setCreatingDoc] = useState(false)
- // Refs for synchronized scrolling
const leftPanelRef = useRef(null)
const rightPanelRef = useRef(null)
- const editorRef = useRef(null)
- const fileInputRef = useRef(null)
- const isScrolling = useRef(false)
- useEffect(() => {
- loadDocuments()
- }, [])
+ const {
+ editorRef, fileInputRef, uploading, uploadError, setUploadError,
+ formatDoc, formatBlock, insertLink, updateEditorContent,
+ handleWordUpload, handlePaste,
+ } = useRichTextEditor(setEditedContent)
- useEffect(() => {
- if (selectedDocument) {
- loadVersions(selectedDocument.id)
- }
- }, [selectedDocument])
-
- // Synchronized scrolling setup
- const setupSyncScroll = useCallback(() => {
- const leftPanel = leftPanelRef.current
- const rightPanel = rightPanelRef.current
-
- if (!leftPanel || !rightPanel) return
-
- const handleLeftScroll = () => {
- if (isScrolling.current) return
- isScrolling.current = true
-
- const leftScrollPercent = leftPanel.scrollTop / (leftPanel.scrollHeight - leftPanel.clientHeight || 1)
- const rightMaxScroll = rightPanel.scrollHeight - rightPanel.clientHeight
- rightPanel.scrollTop = leftScrollPercent * rightMaxScroll
-
- setTimeout(() => { isScrolling.current = false }, 10)
- }
-
- const handleRightScroll = () => {
- if (isScrolling.current) return
- isScrolling.current = true
-
- const rightScrollPercent = rightPanel.scrollTop / (rightPanel.scrollHeight - rightPanel.clientHeight || 1)
- const leftMaxScroll = leftPanel.scrollHeight - leftPanel.clientHeight
- leftPanel.scrollTop = rightScrollPercent * leftMaxScroll
-
- setTimeout(() => { isScrolling.current = false }, 10)
- }
-
- leftPanel.addEventListener('scroll', handleLeftScroll)
- rightPanel.addEventListener('scroll', handleRightScroll)
-
- return () => {
- leftPanel.removeEventListener('scroll', handleLeftScroll)
- rightPanel.removeEventListener('scroll', handleRightScroll)
- }
- }, [])
-
- useEffect(() => {
- const cleanup = setupSyncScroll()
- return cleanup
- }, [setupSyncScroll, currentVersion, draftVersion])
+ useSyncScroll(leftPanelRef, rightPanelRef, [currentVersion, draftVersion])
const loadDocuments = async () => {
setLoading(true)
@@ -200,299 +102,22 @@ export default function WorkflowPage() {
}
}
- // Rich text editor functions
- const formatDoc = (cmd: string, value: string | null = null) => {
- if (editorRef.current) {
- editorRef.current.focus()
- document.execCommand(cmd, false, value || undefined)
- updateEditorContent()
+ useEffect(() => {
+ loadDocuments()
+ }, [])
+
+ useEffect(() => {
+ if (selectedDocument) {
+ loadVersions(selectedDocument.id)
}
- }
+ }, [selectedDocument])
- const formatBlock = (tag: string) => {
- if (editorRef.current) {
- editorRef.current.focus()
- document.execCommand('formatBlock', false, `<${tag}>`)
- updateEditorContent()
- }
- }
-
- const insertLink = () => {
- const url = prompt('Link-URL eingeben:', 'https://')
- if (url && editorRef.current) {
- editorRef.current.focus()
- document.execCommand('createLink', false, url)
- updateEditorContent()
- }
- }
-
- const updateEditorContent = () => {
- if (editorRef.current) {
- setEditedContent(editorRef.current.innerHTML)
- }
- }
-
- // Word document upload
- const handleWordUpload = async (event: React.ChangeEvent) => {
- const file = event.target.files?.[0]
- if (!file) return
-
- setUploading(true)
-
- const formData = new FormData()
- formData.append('file', file)
-
- try {
- const response = await fetch('/api/admin/consent/versions/upload-word', {
- method: 'POST',
- body: formData
- })
-
- if (response.ok) {
- const data = await response.json()
- if (editorRef.current) {
- editorRef.current.innerHTML = data.html || 'Konvertierung fehlgeschlagen
'
- setEditedContent(editorRef.current.innerHTML)
- }
- } else {
- const errorData = await response.json().catch(() => ({}))
- setUploadError('Fehler beim Importieren: ' + (errorData.detail || 'Unbekannter Fehler'))
- }
- } catch (e) {
- setUploadError('Fehler beim Hochladen: ' + (e instanceof Error ? e.message : 'Unbekannter Fehler'))
- } finally {
- setUploading(false)
- if (fileInputRef.current) {
- fileInputRef.current.value = ''
- }
- }
- }
-
- // Clean Word HTML on paste
- const handlePaste = (e: React.ClipboardEvent) => {
- const clipboardData = e.clipboardData
- const html = clipboardData.getData('text/html')
-
- if (html) {
- e.preventDefault()
- const cleanHtml = cleanWordHtml(html)
- document.execCommand('insertHTML', false, cleanHtml)
- updateEditorContent()
- }
- }
-
- const cleanWordHtml = (html: string): string => {
- let cleaned = html
- cleaned = cleaned.replace(/\s*mso-[^:]+:[^;]+;?/gi, '')
- cleaned = cleaned.replace(/\s*style="[^"]*"/gi, '')
- cleaned = cleaned.replace(/\s*class="[^"]*"/gi, '')
- cleaned = cleaned.replace(/<\/o:p>/gi, '')
- cleaned = cleaned.replace(/<\/?o:[^>]*>/gi, '')
- cleaned = cleaned.replace(/<\/?w:[^>]*>/gi, '')
- cleaned = cleaned.replace(/<\/?m:[^>]*>/gi, '')
- cleaned = cleaned.replace(/]*>\s*<\/span>/gi, '')
- return cleaned
- }
-
- const createNewDraft = async () => {
- if (!selectedDocument) return
- setSaving(true)
- try {
- const nextVersion = getNextVersionNumber()
- const res = await fetch('/api/admin/consent/versions', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- document_id: selectedDocument.id,
- version: nextVersion,
- language: 'de',
- title: editedTitle || currentVersion?.title || selectedDocument.name,
- content: editedContent || currentVersion?.content || '',
- summary: editedSummary || currentVersion?.summary || '',
- }),
- })
-
- if (res.ok) {
- await loadVersions(selectedDocument.id)
- } else {
- const err = await res.json()
- setError(err.error || 'Fehler beim Erstellen des Entwurfs')
- }
- } catch {
- setError('Fehler beim Erstellen des Entwurfs')
- } finally {
- setSaving(false)
- }
- }
-
- const saveDraft = async () => {
- if (!draftVersion || draftVersion.status !== 'draft') return
- setSaving(true)
- try {
- const res = await fetch(`/api/admin/consent/versions/${draftVersion.id}`, {
- method: 'PUT',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- title: editedTitle,
- content: editedContent,
- summary: editedSummary,
- }),
- })
-
- if (res.ok) {
- await loadVersions(selectedDocument!.id)
- } else {
- const err = await res.json()
- setError(err.error || 'Fehler beim Speichern')
- }
- } catch {
- setError('Fehler beim Speichern')
- } finally {
- setSaving(false)
- }
- }
-
- const submitForReview = async () => {
- if (!draftVersion) return
- setSaving(true)
- try {
- const res = await fetch(`/api/admin/consent/versions/${draftVersion.id}/submit-review`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- })
-
- if (res.ok) {
- await loadVersions(selectedDocument!.id)
- } else {
- const err = await res.json()
- setError(err.error || 'Fehler beim Einreichen')
- }
- } catch {
- setError('Fehler beim Einreichen zur Pruefung')
- } finally {
- setSaving(false)
- }
- }
-
- const approveVersion = async () => {
- if (!draftVersion) return
- setSaving(true)
- try {
- const res = await fetch(`/api/admin/consent/versions/${draftVersion.id}/approve`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ comment: approvalComment }),
- })
-
- if (res.ok) {
- setShowApprovalModal(null)
- setApprovalComment('')
- await loadVersions(selectedDocument!.id)
- } else {
- const err = await res.json()
- setError(err.error || 'Fehler bei der Freigabe')
- }
- } catch {
- setError('Fehler bei der Freigabe')
- } finally {
- setSaving(false)
- }
- }
-
- const rejectVersion = async () => {
- if (!draftVersion) return
- setSaving(true)
- try {
- const res = await fetch(`/api/admin/consent/versions/${draftVersion.id}/reject`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ reason: approvalComment }),
- })
-
- if (res.ok) {
- setShowApprovalModal(null)
- setApprovalComment('')
- await loadVersions(selectedDocument!.id)
- } else {
- const err = await res.json()
- setError(err.error || 'Fehler bei der Ablehnung')
- }
- } catch {
- setError('Fehler bei der Ablehnung')
- } finally {
- setSaving(false)
- }
- }
-
- const publishVersion = async () => {
- if (!draftVersion || draftVersion.status !== 'approved') return
- if (!confirm('Version wirklich veroeffentlichen? Die aktuelle Version wird archiviert.')) return
-
- setSaving(true)
- try {
- const res = await fetch(`/api/admin/consent/versions/${draftVersion.id}/publish`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- })
-
- if (res.ok) {
- await loadVersions(selectedDocument!.id)
- } else {
- const err = await res.json()
- setError(err.error || 'Fehler beim Veroeffentlichen')
- }
- } catch {
- setError('Fehler beim Veroeffentlichen')
- } finally {
- setSaving(false)
- }
- }
-
- const createDocument = async () => {
- if (!newDocForm.name.trim()) return
- setCreatingDoc(true)
- try {
- const res = await fetch('/api/admin/consent/documents', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(newDocForm),
- })
- if (res.ok) {
- const newDoc: Document = await res.json()
- setDocuments(prev => [newDoc, ...prev])
- setSelectedDocument(newDoc)
- setShowNewDocModal(false)
- setNewDocForm({ type: 'privacy_policy', name: '', description: '' })
- } else {
- setError('Fehler beim Erstellen des Dokuments')
- }
- } catch {
- setError('Verbindungsfehler beim Erstellen')
- } finally {
- setCreatingDoc(false)
- }
- }
-
- const getNextVersionNumber = () => {
- if (versions.length === 0) return '1.0'
- const latest = versions[0]
- const parts = latest.version.split('.')
- const major = parseInt(parts[0]) || 1
- const minor = parseInt(parts[1]) || 0
- return `${major}.${minor + 1}`
- }
-
- const loadApprovalHistory = async (versionId: string) => {
- try {
- const res = await fetch(`/api/admin/consent/versions/${versionId}/approval-history`)
- if (res.ok) {
- const data = await res.json()
- setApprovalHistory(data.approval_history || [])
- }
- } catch {
- console.error('Failed to load approval history')
- }
- }
+ const actions = useWorkflowActions({
+ selectedDocument, setDocuments, setSelectedDocument,
+ draftVersion, currentVersion, versions,
+ editedTitle, editedContent, editedSummary,
+ loadVersions, setError,
+ })
const isEditable = draftVersion?.status === 'draft' || !draftVersion
@@ -500,7 +125,6 @@ export default function WorkflowPage() {
- {/* Error Banner */}
{error && (
{error}
@@ -512,69 +136,21 @@ export default function WorkflowPage() {
)}
- {/* Document Selector */}
-
-
-
-
-
-
-
-
- {currentVersion && (
-
- Aktuelle Version: v{currentVersion.version}
-
- )}
- {draftVersion && (
-
- {STATUS_LABELS[draftVersion.status].label}: v{draftVersion.version}
-
- )}
-
-
-
-
-
-
-
-
-
+
actions.setShowNewDocModal(true)}
+ onCompareView={() => setShowCompareView(true)}
+ onToggleHistory={() => {
+ setShowHistory(!showHistory)
+ if (draftVersion && !showHistory) {
+ actions.loadApprovalHistory(draftVersion.id)
+ }
+ }}
+ />
{loading ? (
@@ -586,187 +162,25 @@ export default function WorkflowPage() {
) : (
<>
- {/* Workflow Status Bar */}
-
-
-
- {['draft', 'review', 'approved', 'published'].map((status, idx) => (
-
- {idx > 0 &&
}
-
-
{idx + 1}
-
- {status === 'draft' ? 'Entwurf' :
- status === 'review' ? 'Pruefung' :
- status === 'approved' ? 'Freigegeben' : 'Veroeffentlicht'}
-
-
-
- ))}
-
+
- {/* Action Buttons */}
-
- {!draftVersion && (
-
- )}
-
- {draftVersion?.status === 'draft' && (
- <>
-
-
- >
- )}
-
- {draftVersion?.status === 'review' && (
- <>
-
-
- >
- )}
-
- {draftVersion?.status === 'approved' && (
-
- )}
-
- {draftVersion?.status === 'rejected' && (
-
- Abgelehnt: {draftVersion.rejection_reason}
-
-
- )}
-
-
-
-
- {/* Rich Text Toolbar - only shown when editable */}
{isEditable && (
-
-
- {/* Formatting */}
-
-
-
-
-
-
- {/* Headings */}
-
-
-
-
-
-
-
- {/* Lists */}
-
-
-
-
-
- {/* Links */}
-
-
- {/* Word Upload */}
-
-
-
-
-
-
+
)}
{uploadError && (
@@ -776,399 +190,66 @@ export default function WorkflowPage() {
)}
- {/* Split View Editor - Synchronized Scrolling */}
-
- {/* Left: Current Published Version */}
-
-
-
-
Veroeffentlichte Version
- {currentVersion && (
-
v{currentVersion.version}
- )}
-
-
- Nur Lesen
-
-
-
- {currentVersion ? (
- <>
-
-
- >
- ) : (
-
- Keine veroeffentlichte Version vorhanden
-
- )}
-
-
+
- {/* Right: Draft/Edit Version */}
-
-
-
-
- {draftVersion ? 'Aenderungsversion' : 'Neue Version'}
-
- {draftVersion && (
-
- v{draftVersion.version} - {STATUS_LABELS[draftVersion.status].label}
-
- )}
-
- {isEditable && (
-
- Bearbeitbar
-
- )}
-
-
-
setEditedTitle(e.target.value)}
- disabled={!isEditable}
- placeholder="Titel der Version..."
- className={`w-full px-3 py-2 mb-4 border rounded-lg ${
- isEditable ? 'border-slate-300 bg-white' : 'border-slate-200 bg-slate-50 text-slate-700'
- }`}
- />
-
- {isEditable ? (
-
- ) : (
-
- )}
-
- {/* Character count */}
-
- {(editorRef.current?.textContent || editedContent.replace(/<[^>]*>/g, '')).length} Zeichen
-
-
-
-
-
- {/* History Panel */}
{showHistory && (
-
-
Genehmigungsverlauf
- {approvalHistory.length > 0 ? (
-
- {approvalHistory.map((item, idx) => (
-
- {item.action}
- {item.approver || 'System'}
- {item.comment && (
- "{item.comment}"
- )}
-
- {new Date(item.created_at).toLocaleString('de-DE')}
-
-
- ))}
-
- ) : (
-
Keine Genehmigungshistorie vorhanden.
- )}
-
-
Alle Versionen
-
- {versions.map((v) => (
-
-
-
- v{v.version}
-
- {STATUS_LABELS[v.status].label}
-
- {v.title}
-
-
- {new Date(v.created_at).toLocaleDateString('de-DE')}
-
-
-
- ))}
-
-
+
)}
>
)}
- {/* Full Screen Compare View */}
{showCompareView && (
-
- {/* Header */}
-
-
-
Versionsvergleich
-
- {currentVersion ? `v${currentVersion.version}` : 'Keine Version'}
- vs
- {draftVersion ? `v${draftVersion.version}` : 'Neue Version'}
-
-
-
-
-
- {/* Compare Panels */}
-
- {/* Left: Published */}
-
-
- Veroeffentlichte Version
- {currentVersion && (
- v{currentVersion.version}
- )}
-
-
- {currentVersion ? (
-
- ) : (
-
Keine veroeffentlichte Version
- )}
-
-
-
- {/* Right: Draft */}
-
-
-
- {draftVersion ? 'Aenderungsversion' : 'Neue Version'}
-
- {draftVersion && (
-
- v{draftVersion.version} - {STATUS_LABELS[draftVersion.status].label}
-
- )}
-
-
-
-
-
- {/* Footer with Actions */}
-
- {draftVersion?.status === 'draft' && (
- <>
-
-
- >
- )}
- {draftVersion?.status === 'review' && (
- <>
-
-
- >
- )}
- {draftVersion?.status === 'approved' && (
-
- )}
-
-
+ setShowCompareView(false)}
+ onSaveDraft={actions.saveDraft}
+ onSubmitForReview={actions.submitForReview}
+ onShowApprovalModal={actions.setShowApprovalModal}
+ onPublishVersion={actions.publishVersion}
+ />
)}
- {/* New Document Modal */}
- {showNewDocModal && (
-
-
-
-
Neues Dokument erstellen
-
-
-
-
-
-
-
-
- setNewDocForm({ ...newDocForm, name: e.target.value })}
- placeholder="z.B. Datenschutzerklärung Website"
- className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
- />
-
-
-
-
-
-
-
-
-
-
-
+ {actions.showNewDocModal && (
+ actions.setShowNewDocModal(false)}
+ onCreate={actions.createDocument}
+ creatingDoc={actions.creatingDoc}
+ />
)}
- {/* Approval Modal */}
- {showApprovalModal && (
-
-
-
- {showApprovalModal === 'approve' ? 'Version freigeben' : 'Version ablehnen'}
-
-
-
+ {actions.showApprovalModal && (
+ {
+ actions.setShowApprovalModal(null)
+ actions.setApprovalComment('')
+ }}
+ onConfirm={actions.showApprovalModal === 'approve' ? actions.approveVersion : actions.rejectVersion}
+ saving={actions.saving}
+ />
)}
)