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>
257 lines
9.0 KiB
TypeScript
257 lines
9.0 KiB
TypeScript
'use client'
|
|
|
|
/**
|
|
* Document Workflow Page (SDK Version)
|
|
*
|
|
* Split-view editor for legal documents with synchronized scrolling:
|
|
* - Left: Current published version (read-only)
|
|
* - Right: Draft/new version (editable)
|
|
* - Approval workflow: Draft -> Review -> Approved -> Published
|
|
* - DOCX upload with Word conversion
|
|
* - Rich text editor with formatting toolbar
|
|
*/
|
|
|
|
import { useState, useEffect, useRef } from 'react'
|
|
import { useSDK } from '@/lib/sdk'
|
|
import StepHeader from '@/components/sdk/StepHeader/StepHeader'
|
|
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() {
|
|
useSDK()
|
|
const [documents, setDocuments] = useState<Document[]>([])
|
|
const [versions, setVersions] = useState<Version[]>([])
|
|
const [selectedDocument, setSelectedDocument] = useState<Document | null>(null)
|
|
const [currentVersion, setCurrentVersion] = useState<Version | null>(null)
|
|
const [draftVersion, setDraftVersion] = useState<Version | null>(null)
|
|
const [loading, setLoading] = useState(true)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [editedContent, setEditedContent] = useState('')
|
|
const [editedTitle, setEditedTitle] = useState('')
|
|
const [editedSummary, setEditedSummary] = useState('')
|
|
const [showHistory, setShowHistory] = useState(false)
|
|
const [showCompareView, setShowCompareView] = useState(false)
|
|
|
|
const leftPanelRef = useRef<HTMLDivElement>(null)
|
|
const rightPanelRef = useRef<HTMLDivElement>(null)
|
|
|
|
const {
|
|
editorRef, fileInputRef, uploading, uploadError, setUploadError,
|
|
formatDoc, formatBlock, insertLink, updateEditorContent,
|
|
handleWordUpload, handlePaste,
|
|
} = useRichTextEditor(setEditedContent)
|
|
|
|
useSyncScroll(leftPanelRef, rightPanelRef, [currentVersion, draftVersion])
|
|
|
|
const loadDocuments = async () => {
|
|
setLoading(true)
|
|
try {
|
|
const res = await fetch('/api/admin/consent/documents')
|
|
if (res.ok) {
|
|
const data = await res.json()
|
|
setDocuments(data.documents || [])
|
|
if (data.documents?.length > 0 && !selectedDocument) {
|
|
setSelectedDocument(data.documents[0])
|
|
}
|
|
}
|
|
} catch {
|
|
setError('Fehler beim Laden der Dokumente')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const loadVersions = async (docId: string) => {
|
|
try {
|
|
const res = await fetch(`/api/admin/consent/documents/${docId}/versions`)
|
|
if (res.ok) {
|
|
const data = await res.json()
|
|
const versionList = Array.isArray(data) ? data : (data.versions || [])
|
|
setVersions(versionList)
|
|
|
|
const published = versionList.find((v: Version) => v.status === 'published')
|
|
setCurrentVersion(published || null)
|
|
|
|
const draft = versionList.find((v: Version) =>
|
|
v.status === 'draft' || v.status === 'review' || v.status === 'approved'
|
|
)
|
|
if (draft) {
|
|
setDraftVersion(draft)
|
|
setEditedContent(draft.content)
|
|
setEditedTitle(draft.title)
|
|
setEditedSummary(draft.summary || '')
|
|
} else {
|
|
setDraftVersion(null)
|
|
setEditedContent(published?.content || '')
|
|
setEditedTitle(published?.title || '')
|
|
setEditedSummary(published?.summary || '')
|
|
}
|
|
}
|
|
} catch {
|
|
setError('Fehler beim Laden der Versionen')
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
loadDocuments()
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
if (selectedDocument) {
|
|
loadVersions(selectedDocument.id)
|
|
}
|
|
}, [selectedDocument])
|
|
|
|
const actions = useWorkflowActions({
|
|
selectedDocument, setDocuments, setSelectedDocument,
|
|
draftVersion, currentVersion, versions,
|
|
editedTitle, editedContent, editedSummary,
|
|
loadVersions, setError,
|
|
})
|
|
|
|
const isEditable = draftVersion?.status === 'draft' || !draftVersion
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<StepHeader stepId="workflow" showProgress={true} />
|
|
|
|
{error && (
|
|
<div className="bg-red-50 border border-red-200 rounded-lg p-4 flex items-center justify-between">
|
|
<span className="text-red-700">{error}</span>
|
|
<button onClick={() => setError(null)} className="text-red-500 hover:text-red-700">
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
<DocumentSelectorBar
|
|
documents={documents}
|
|
selectedDocument={selectedDocument}
|
|
onSelectDocument={setSelectedDocument}
|
|
currentVersion={currentVersion}
|
|
draftVersion={draftVersion}
|
|
onNewDocument={() => actions.setShowNewDocModal(true)}
|
|
onCompareView={() => setShowCompareView(true)}
|
|
onToggleHistory={() => {
|
|
setShowHistory(!showHistory)
|
|
if (draftVersion && !showHistory) {
|
|
actions.loadApprovalHistory(draftVersion.id)
|
|
}
|
|
}}
|
|
/>
|
|
|
|
{loading ? (
|
|
<div className="flex items-center justify-center h-64">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-purple-600" />
|
|
</div>
|
|
) : !selectedDocument ? (
|
|
<div className="bg-white rounded-xl shadow-sm border p-12 text-center text-slate-500">
|
|
Bitte waehlen Sie ein Dokument aus
|
|
</div>
|
|
) : (
|
|
<>
|
|
<WorkflowStatusBar
|
|
draftVersion={draftVersion}
|
|
saving={actions.saving}
|
|
onCreateNewDraft={actions.createNewDraft}
|
|
onSaveDraft={actions.saveDraft}
|
|
onSubmitForReview={actions.submitForReview}
|
|
onShowApprovalModal={actions.setShowApprovalModal}
|
|
onPublishVersion={actions.publishVersion}
|
|
/>
|
|
|
|
{isEditable && (
|
|
<RichTextToolbar
|
|
fileInputRef={fileInputRef}
|
|
uploading={uploading}
|
|
onFormatDoc={formatDoc}
|
|
onFormatBlock={formatBlock}
|
|
onInsertLink={insertLink}
|
|
onWordUpload={handleWordUpload}
|
|
/>
|
|
)}
|
|
|
|
{uploadError && (
|
|
<div className="bg-red-50 border border-red-200 rounded p-3 text-sm text-red-700 flex items-center justify-between">
|
|
<span>{uploadError}</span>
|
|
<button onClick={() => setUploadError(null)} className="ml-4 text-red-500 hover:text-red-700">✕</button>
|
|
</div>
|
|
)}
|
|
|
|
<SplitViewEditor
|
|
leftPanelRef={leftPanelRef}
|
|
rightPanelRef={rightPanelRef}
|
|
editorRef={editorRef}
|
|
currentVersion={currentVersion}
|
|
draftVersion={draftVersion}
|
|
isEditable={isEditable}
|
|
editedTitle={editedTitle}
|
|
editedContent={editedContent}
|
|
onTitleChange={setEditedTitle}
|
|
onUpdateEditorContent={updateEditorContent}
|
|
onPaste={handlePaste}
|
|
/>
|
|
|
|
{showHistory && (
|
|
<HistoryPanel
|
|
approvalHistory={actions.approvalHistory}
|
|
versions={versions}
|
|
draftVersion={draftVersion}
|
|
currentVersion={currentVersion}
|
|
/>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{showCompareView && (
|
|
<CompareView
|
|
currentVersion={currentVersion}
|
|
draftVersion={draftVersion}
|
|
editedContent={editedContent}
|
|
onClose={() => setShowCompareView(false)}
|
|
onSaveDraft={actions.saveDraft}
|
|
onSubmitForReview={actions.submitForReview}
|
|
onShowApprovalModal={actions.setShowApprovalModal}
|
|
onPublishVersion={actions.publishVersion}
|
|
/>
|
|
)}
|
|
|
|
{actions.showNewDocModal && (
|
|
<NewDocumentModal
|
|
newDocForm={actions.newDocForm}
|
|
onChange={actions.setNewDocForm}
|
|
onClose={() => actions.setShowNewDocModal(false)}
|
|
onCreate={actions.createDocument}
|
|
creatingDoc={actions.creatingDoc}
|
|
/>
|
|
)}
|
|
|
|
{actions.showApprovalModal && (
|
|
<ApprovalModal
|
|
mode={actions.showApprovalModal}
|
|
approvalComment={actions.approvalComment}
|
|
onCommentChange={actions.setApprovalComment}
|
|
onCancel={() => {
|
|
actions.setShowApprovalModal(null)
|
|
actions.setApprovalComment('')
|
|
}}
|
|
onConfirm={actions.showApprovalModal === 'approve' ? actions.approveVersion : actions.rejectVersion}
|
|
saving={actions.saving}
|
|
/>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|