refactor(admin): split workflow page.tsx into colocated components
Split 1175-LOC workflow page into _components, _hooks and _types modules. page.tsx now 256 LOC (wire-up only). Behavior preserved. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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 (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-xl shadow-xl p-6 w-full max-w-md">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">
|
||||
{mode === 'approve' ? 'Version freigeben' : 'Version ablehnen'}
|
||||
</h3>
|
||||
<textarea
|
||||
value={approvalComment}
|
||||
onChange={(e) => onCommentChange(e.target.value)}
|
||||
placeholder={mode === 'approve' ? 'Kommentar (optional)...' : 'Ablehnungsgrund...'}
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg min-h-[100px] mb-4"
|
||||
required={mode === 'reject'}
|
||||
/>
|
||||
<div className="flex justify-end gap-3">
|
||||
<button
|
||||
onClick={onCancel}
|
||||
className="px-4 py-2 text-slate-600 hover:text-slate-900"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
onClick={onConfirm}
|
||||
disabled={saving || (mode === 'reject' && !approvalComment)}
|
||||
className={`px-4 py-2 text-white rounded-lg disabled:opacity-50 ${
|
||||
mode === 'approve'
|
||||
? 'bg-green-600 hover:bg-green-700'
|
||||
: 'bg-red-600 hover:bg-red-700'
|
||||
}`}
|
||||
>
|
||||
{saving ? 'Wird verarbeitet...' : mode === 'approve' ? 'Freigeben' : 'Ablehnen'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
143
admin-compliance/app/sdk/workflow/_components/CompareView.tsx
Normal file
143
admin-compliance/app/sdk/workflow/_components/CompareView.tsx
Normal file
@@ -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 (
|
||||
<div className="fixed inset-0 bg-slate-900 z-50 flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="bg-slate-800 border-b border-slate-700 px-6 py-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<h2 className="text-xl font-semibold text-white">Versionsvergleich</h2>
|
||||
<span className="text-slate-400">
|
||||
{currentVersion ? `v${currentVersion.version}` : 'Keine Version'}
|
||||
<span className="mx-2 text-slate-600">vs</span>
|
||||
{draftVersion ? `v${draftVersion.version}` : 'Neue Version'}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 bg-slate-700 text-white rounded-lg hover:bg-slate-600"
|
||||
>
|
||||
Schliessen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Compare Panels */}
|
||||
<div className="flex-1 grid grid-cols-2 gap-1 bg-slate-700">
|
||||
{/* Left: Published */}
|
||||
<div className="bg-white flex flex-col">
|
||||
<div className="bg-emerald-100 border-b border-emerald-200 px-4 py-2">
|
||||
<span className="font-medium text-emerald-800">Veroeffentlichte Version</span>
|
||||
{currentVersion && (
|
||||
<span className="ml-2 text-emerald-600">v{currentVersion.version}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-6 prose prose-sm max-w-none">
|
||||
{currentVersion ? (
|
||||
<div dangerouslySetInnerHTML={{ __html: currentVersion.content }} />
|
||||
) : (
|
||||
<p className="text-slate-500 text-center py-12">Keine veroeffentlichte Version</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: Draft */}
|
||||
<div className="bg-white flex flex-col">
|
||||
<div className={`border-b px-4 py-2 ${
|
||||
draftVersion?.status === 'draft' ? 'bg-yellow-100 border-yellow-200' :
|
||||
draftVersion?.status === 'review' ? 'bg-blue-100 border-blue-200' :
|
||||
draftVersion?.status === 'approved' ? 'bg-green-100 border-green-200' :
|
||||
'bg-slate-100 border-slate-200'
|
||||
}`}>
|
||||
<span className={`font-medium ${
|
||||
draftVersion?.status === 'draft' ? 'text-yellow-800' :
|
||||
draftVersion?.status === 'review' ? 'text-blue-800' :
|
||||
draftVersion?.status === 'approved' ? 'text-green-800' :
|
||||
'text-slate-800'
|
||||
}`}>
|
||||
{draftVersion ? 'Aenderungsversion' : 'Neue Version'}
|
||||
</span>
|
||||
{draftVersion && (
|
||||
<span className={`ml-2 ${
|
||||
draftVersion.status === 'draft' ? 'text-yellow-600' :
|
||||
draftVersion.status === 'review' ? 'text-blue-600' :
|
||||
draftVersion.status === 'approved' ? 'text-green-600' :
|
||||
'text-slate-600'
|
||||
}`}>
|
||||
v{draftVersion.version} - {STATUS_LABELS[draftVersion.status].label}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-6 prose prose-sm max-w-none">
|
||||
<div dangerouslySetInnerHTML={{ __html: editedContent || draftVersion?.content || '' }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer with Actions */}
|
||||
<div className="bg-slate-800 border-t border-slate-700 px-6 py-4 flex items-center justify-end gap-3">
|
||||
{draftVersion?.status === 'draft' && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => { onClose(); onSaveDraft() }}
|
||||
className="px-4 py-2 bg-slate-600 text-white rounded-lg hover:bg-slate-500"
|
||||
>
|
||||
Speichern
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { onClose(); onSubmitForReview() }}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-500"
|
||||
>
|
||||
Zur Pruefung einreichen
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{draftVersion?.status === 'review' && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => { onClose(); onShowApprovalModal('reject') }}
|
||||
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-500"
|
||||
>
|
||||
Ablehnen
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { onClose(); onShowApprovalModal('approve') }}
|
||||
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-500"
|
||||
>
|
||||
Freigeben
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{draftVersion?.status === 'approved' && (
|
||||
<button
|
||||
onClick={() => { onClose(); onPublishVersion() }}
|
||||
className="px-4 py-2 bg-emerald-600 text-white rounded-lg hover:bg-emerald-500"
|
||||
>
|
||||
Veroeffentlichen
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="bg-white rounded-xl shadow-sm border p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<label className="text-sm font-medium text-slate-700">Dokument:</label>
|
||||
<select
|
||||
value={selectedDocument?.id || ''}
|
||||
onChange={(e) => {
|
||||
const doc = documents.find(d => d.id === e.target.value)
|
||||
onSelectDocument(doc || null)
|
||||
}}
|
||||
className="px-3 py-2 border border-slate-300 rounded-lg text-sm min-w-[250px]"
|
||||
>
|
||||
<option value="">Dokument auswaehlen...</option>
|
||||
{documents.map((doc) => (
|
||||
<option key={doc.id} value={doc.id}>
|
||||
{DOCUMENT_TYPES[doc.type] || doc.type} - {doc.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{currentVersion && (
|
||||
<span className="text-sm text-slate-500">
|
||||
Aktuelle Version: <span className="font-medium">v{currentVersion.version}</span>
|
||||
</span>
|
||||
)}
|
||||
{draftVersion && (
|
||||
<span className={`px-2 py-1 rounded text-xs ${STATUS_LABELS[draftVersion.status].color}`}>
|
||||
{STATUS_LABELS[draftVersion.status].label}: v{draftVersion.version}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={onNewDocument}
|
||||
className="px-3 py-2 text-sm text-white bg-purple-600 hover:bg-purple-700 rounded-lg"
|
||||
>
|
||||
+ Neues Dokument
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={onCompareView}
|
||||
className="px-3 py-2 text-sm text-purple-600 hover:text-purple-800 border border-purple-300 rounded-lg hover:bg-purple-50"
|
||||
title="Vollbild-Vergleich"
|
||||
>
|
||||
Vollbild
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={onToggleHistory}
|
||||
className="px-3 py-2 text-sm text-slate-600 hover:text-slate-900 border border-slate-300 rounded-lg hover:bg-slate-50"
|
||||
>
|
||||
Historie
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Genehmigungsverlauf</h3>
|
||||
{approvalHistory.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{approvalHistory.map((item, idx) => (
|
||||
<div key={idx} className="flex items-center gap-4 p-3 border border-slate-200 rounded-lg">
|
||||
<span className={`px-2 py-1 rounded text-xs ${
|
||||
item.action === 'approved' ? 'bg-green-100 text-green-700' :
|
||||
item.action === 'rejected' ? 'bg-red-100 text-red-700' :
|
||||
item.action === 'submitted' ? 'bg-blue-100 text-blue-700' :
|
||||
'bg-slate-100 text-slate-700'
|
||||
}`}>{item.action}</span>
|
||||
<span className="text-sm text-slate-600">{item.approver || 'System'}</span>
|
||||
{item.comment && (
|
||||
<span className="text-sm text-slate-500 italic">"{item.comment}"</span>
|
||||
)}
|
||||
<span className="text-sm text-slate-400 ml-auto">
|
||||
{new Date(item.created_at).toLocaleString('de-DE')}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-slate-500">Keine Genehmigungshistorie vorhanden.</p>
|
||||
)}
|
||||
|
||||
<h3 className="text-lg font-semibold text-slate-900 mt-6 mb-4">Alle Versionen</h3>
|
||||
<div className="space-y-3">
|
||||
{versions.map((v) => (
|
||||
<div
|
||||
key={v.id}
|
||||
className={`p-4 border rounded-lg ${
|
||||
v.id === draftVersion?.id ? 'border-purple-300 bg-purple-50' :
|
||||
v.id === currentVersion?.id ? 'border-emerald-300 bg-emerald-50' :
|
||||
'border-slate-200'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="font-mono font-medium">v{v.version}</span>
|
||||
<span className={`px-2 py-0.5 rounded text-xs ${STATUS_LABELS[v.status].color}`}>
|
||||
{STATUS_LABELS[v.status].label}
|
||||
</span>
|
||||
<span className="text-sm text-slate-500">{v.title}</span>
|
||||
</div>
|
||||
<span className="text-sm text-slate-400">
|
||||
{new Date(v.created_at).toLocaleDateString('de-DE')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-md">
|
||||
<div className="px-6 py-4 border-b border-gray-200">
|
||||
<h2 className="text-lg font-bold text-gray-900">Neues Dokument erstellen</h2>
|
||||
</div>
|
||||
<div className="p-6 space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Dokumenttyp</label>
|
||||
<select
|
||||
value={newDocForm.type}
|
||||
onChange={(e) => onChange({ ...newDocForm, type: e.target.value })}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
|
||||
>
|
||||
<option value="privacy_policy">Datenschutzerklärung</option>
|
||||
<option value="terms">AGB</option>
|
||||
<option value="cookie_policy">Cookie-Richtlinie</option>
|
||||
<option value="imprint">Impressum</option>
|
||||
<option value="dpa">AVV (Auftragsverarbeitung)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newDocForm.name}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Beschreibung (optional)</label>
|
||||
<textarea
|
||||
rows={2}
|
||||
value={newDocForm.description}
|
||||
onChange={(e) => onChange({ ...newDocForm, description: e.target.value })}
|
||||
placeholder="Kurze Beschreibung..."
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-6 py-4 border-t border-gray-200 flex justify-end gap-3">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-lg"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
onClick={onCreate}
|
||||
disabled={creatingDoc || !newDocForm.name.trim()}
|
||||
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50"
|
||||
>
|
||||
{creatingDoc ? 'Erstellen...' : 'Erstellen'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
'use client'
|
||||
|
||||
import { RefObject } from 'react'
|
||||
|
||||
interface RichTextToolbarProps {
|
||||
fileInputRef: RefObject<HTMLInputElement | null>
|
||||
uploading: boolean
|
||||
onFormatDoc: (cmd: string, value?: string | null) => void
|
||||
onFormatBlock: (tag: string) => void
|
||||
onInsertLink: () => void
|
||||
onWordUpload: (event: React.ChangeEvent<HTMLInputElement>) => void
|
||||
}
|
||||
|
||||
export default function RichTextToolbar({
|
||||
fileInputRef,
|
||||
uploading,
|
||||
onFormatDoc,
|
||||
onFormatBlock,
|
||||
onInsertLink,
|
||||
onWordUpload,
|
||||
}: RichTextToolbarProps) {
|
||||
return (
|
||||
<div className="bg-white rounded-xl shadow-sm border p-3">
|
||||
<div className="flex items-center gap-1 flex-wrap">
|
||||
{/* Formatting */}
|
||||
<div className="flex items-center gap-1 border-r border-slate-200 pr-2 mr-2">
|
||||
<button onClick={() => onFormatDoc('bold')} className="p-2 hover:bg-slate-100 rounded" title="Fett">
|
||||
<span className="font-bold">B</span>
|
||||
</button>
|
||||
<button onClick={() => onFormatDoc('italic')} className="p-2 hover:bg-slate-100 rounded" title="Kursiv">
|
||||
<span className="italic">I</span>
|
||||
</button>
|
||||
<button onClick={() => onFormatDoc('underline')} className="p-2 hover:bg-slate-100 rounded" title="Unterstrichen">
|
||||
<span className="underline">U</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Headings */}
|
||||
<div className="flex items-center gap-1 border-r border-slate-200 pr-2 mr-2">
|
||||
<button onClick={() => onFormatBlock('h1')} className="p-2 hover:bg-slate-100 rounded text-sm" title="Ueberschrift 1">
|
||||
H1
|
||||
</button>
|
||||
<button onClick={() => onFormatBlock('h2')} className="p-2 hover:bg-slate-100 rounded text-sm" title="Ueberschrift 2">
|
||||
H2
|
||||
</button>
|
||||
<button onClick={() => onFormatBlock('h3')} className="p-2 hover:bg-slate-100 rounded text-sm" title="Ueberschrift 3">
|
||||
H3
|
||||
</button>
|
||||
<button onClick={() => onFormatBlock('p')} className="p-2 hover:bg-slate-100 rounded text-sm" title="Absatz">
|
||||
P
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Lists */}
|
||||
<div className="flex items-center gap-1 border-r border-slate-200 pr-2 mr-2">
|
||||
<button onClick={() => onFormatDoc('insertUnorderedList')} className="p-2 hover:bg-slate-100 rounded" title="Aufzaehlung">
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
</button>
|
||||
<button onClick={() => onFormatDoc('insertOrderedList')} className="p-2 hover:bg-slate-100 rounded" title="Nummerierung">
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 20l4-16m2 16l4-16M6 9h14M4 15h14" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Links */}
|
||||
<div className="flex items-center gap-1 border-r border-slate-200 pr-2 mr-2">
|
||||
<button onClick={onInsertLink} className="p-2 hover:bg-slate-100 rounded" title="Link einfuegen">
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Word Upload */}
|
||||
<div className="flex items-center gap-1">
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
onChange={onWordUpload}
|
||||
accept=".docx,.doc"
|
||||
className="hidden"
|
||||
/>
|
||||
<button
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={uploading}
|
||||
className="px-3 py-2 bg-blue-50 text-blue-700 hover:bg-blue-100 rounded text-sm flex items-center gap-1"
|
||||
title="Word-Dokument importieren"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
|
||||
</svg>
|
||||
{uploading ? 'Importiere...' : 'Word importieren'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
'use client'
|
||||
|
||||
import { RefObject } from 'react'
|
||||
import { Version, STATUS_LABELS } from '../_types'
|
||||
|
||||
interface SplitViewEditorProps {
|
||||
leftPanelRef: RefObject<HTMLDivElement | null>
|
||||
rightPanelRef: RefObject<HTMLDivElement | null>
|
||||
editorRef: RefObject<HTMLDivElement | null>
|
||||
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 (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* Left: Current Published Version */}
|
||||
<div className="bg-white rounded-xl shadow-sm border overflow-hidden">
|
||||
<div className="bg-emerald-50 border-b border-emerald-200 px-4 py-3 flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="font-semibold text-emerald-900">Veroeffentlichte Version</h3>
|
||||
{currentVersion && (
|
||||
<p className="text-sm text-emerald-700">v{currentVersion.version}</p>
|
||||
)}
|
||||
</div>
|
||||
<span className="px-2 py-1 bg-emerald-100 text-emerald-700 rounded text-xs font-medium">
|
||||
Nur Lesen
|
||||
</span>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
{currentVersion ? (
|
||||
<>
|
||||
<input
|
||||
type="text"
|
||||
value={currentVersion.title}
|
||||
disabled
|
||||
className="w-full px-3 py-2 mb-4 bg-slate-50 border border-slate-200 rounded-lg text-slate-700"
|
||||
/>
|
||||
<div
|
||||
ref={leftPanelRef}
|
||||
className="prose prose-sm max-w-none p-4 bg-slate-50 border border-slate-200 rounded-lg min-h-[500px] max-h-[500px] overflow-y-auto"
|
||||
dangerouslySetInnerHTML={{ __html: currentVersion.content }}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center py-12 text-slate-500">
|
||||
Keine veroeffentlichte Version vorhanden
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: Draft/Edit Version */}
|
||||
<div className="bg-white rounded-xl shadow-sm border overflow-hidden">
|
||||
<div className={`border-b px-4 py-3 flex items-center justify-between ${
|
||||
draftVersion?.status === 'draft' ? 'bg-yellow-50 border-yellow-200' :
|
||||
draftVersion?.status === 'review' ? 'bg-blue-50 border-blue-200' :
|
||||
draftVersion?.status === 'approved' ? 'bg-green-50 border-green-200' :
|
||||
'bg-slate-50 border-slate-200'
|
||||
}`}>
|
||||
<div>
|
||||
<h3 className={`font-semibold ${
|
||||
draftVersion?.status === 'draft' ? 'text-yellow-900' :
|
||||
draftVersion?.status === 'review' ? 'text-blue-900' :
|
||||
draftVersion?.status === 'approved' ? 'text-green-900' :
|
||||
'text-slate-900'
|
||||
}`}>
|
||||
{draftVersion ? 'Aenderungsversion' : 'Neue Version'}
|
||||
</h3>
|
||||
{draftVersion && (
|
||||
<p className={`text-sm ${
|
||||
draftVersion.status === 'draft' ? 'text-yellow-700' :
|
||||
draftVersion.status === 'review' ? 'text-blue-700' :
|
||||
draftVersion.status === 'approved' ? 'text-green-700' :
|
||||
'text-slate-700'
|
||||
}`}>
|
||||
v{draftVersion.version} - {STATUS_LABELS[draftVersion.status].label}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{isEditable && (
|
||||
<span className="px-2 py-1 bg-yellow-100 text-yellow-700 rounded text-xs font-medium">
|
||||
Bearbeitbar
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<input
|
||||
type="text"
|
||||
value={editedTitle}
|
||||
onChange={(e) => 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 ? (
|
||||
<div
|
||||
ref={rightPanelRef}
|
||||
className="min-h-[500px] max-h-[500px] overflow-y-auto"
|
||||
>
|
||||
<div
|
||||
ref={editorRef}
|
||||
contentEditable
|
||||
onInput={onUpdateEditorContent}
|
||||
onPaste={onPaste}
|
||||
className="prose prose-sm max-w-none p-4 bg-white border border-slate-300 rounded-lg min-h-[500px] focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||
dangerouslySetInnerHTML={{ __html: editedContent }}
|
||||
suppressContentEditableWarning
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
ref={rightPanelRef}
|
||||
className="prose prose-sm max-w-none p-4 bg-slate-50 border border-slate-200 rounded-lg min-h-[500px] max-h-[500px] overflow-y-auto"
|
||||
dangerouslySetInnerHTML={{ __html: editedContent || draftVersion?.content || '' }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Character count */}
|
||||
<div className="mt-2 text-right text-sm text-slate-500">
|
||||
{(editorRef.current?.textContent || editedContent.replace(/<[^>]*>/g, '')).length} Zeichen
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="bg-white rounded-xl shadow-sm border p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-6">
|
||||
{['draft', 'review', 'approved', 'published'].map((status, idx) => (
|
||||
<div key={status} className="flex items-center">
|
||||
{idx > 0 && <div className="w-8 h-0.5 bg-slate-200 mr-2" />}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium ${
|
||||
(status === 'draft' && draftVersion?.status === 'draft') ||
|
||||
(status === 'review' && draftVersion?.status === 'review') ||
|
||||
(status === 'approved' && draftVersion?.status === 'approved') ||
|
||||
(status === 'published' && !draftVersion)
|
||||
? 'bg-purple-500 text-white'
|
||||
: 'bg-slate-200 text-slate-600'
|
||||
}`}>{idx + 1}</div>
|
||||
<span className="text-sm text-slate-600">
|
||||
{status === 'draft' ? 'Entwurf' :
|
||||
status === 'review' ? 'Pruefung' :
|
||||
status === 'approved' ? 'Freigegeben' : 'Veroeffentlicht'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex items-center gap-2">
|
||||
{!draftVersion && (
|
||||
<button
|
||||
onClick={onCreateNewDraft}
|
||||
disabled={saving}
|
||||
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 text-sm font-medium"
|
||||
>
|
||||
Neue Version erstellen
|
||||
</button>
|
||||
)}
|
||||
|
||||
{draftVersion?.status === 'draft' && (
|
||||
<>
|
||||
<button
|
||||
onClick={onSaveDraft}
|
||||
disabled={saving}
|
||||
className="px-4 py-2 bg-slate-600 text-white rounded-lg hover:bg-slate-700 disabled:opacity-50 text-sm font-medium"
|
||||
>
|
||||
{saving ? 'Speichern...' : 'Speichern'}
|
||||
</button>
|
||||
<button
|
||||
onClick={onSubmitForReview}
|
||||
disabled={saving}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 text-sm font-medium"
|
||||
>
|
||||
Zur Pruefung einreichen
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{draftVersion?.status === 'review' && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => onShowApprovalModal('reject')}
|
||||
disabled={saving}
|
||||
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 disabled:opacity-50 text-sm font-medium"
|
||||
>
|
||||
Ablehnen
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onShowApprovalModal('approve')}
|
||||
disabled={saving}
|
||||
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50 text-sm font-medium"
|
||||
>
|
||||
Freigeben
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{draftVersion?.status === 'approved' && (
|
||||
<button
|
||||
onClick={onPublishVersion}
|
||||
disabled={saving}
|
||||
className="px-4 py-2 bg-emerald-600 text-white rounded-lg hover:bg-emerald-700 disabled:opacity-50 text-sm font-medium"
|
||||
>
|
||||
Jetzt veroeffentlichen
|
||||
</button>
|
||||
)}
|
||||
|
||||
{draftVersion?.status === 'rejected' && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-red-600">Abgelehnt: {draftVersion.rejection_reason}</span>
|
||||
<button
|
||||
onClick={onCreateNewDraft}
|
||||
disabled={saving}
|
||||
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 text-sm font-medium"
|
||||
>
|
||||
Neu bearbeiten
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user