feat(workflow): 5-Stage Lifecycle UI im Compliance Workflow-Editor
CI / detect-changes (push) Successful in 8s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / build-sha-integrity (push) Failing after 4s
CI / validate-canonical-controls (push) Successful in 10s
CI / loc-budget (push) Successful in 14s
CI / sbom-scan (push) Has been skipped
CI / test-python-backend (push) Has been skipped
CI / iace-gt-coverage (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 2m42s
CI / test-go (push) Has been skipped
CI / test-python-document-crawler (push) Has been skipped

Erweitert Phase 1 (Backend 5-Stage Lifecycle, Migration 148) jetzt auch
im Frontend: Status-Pills, Buttons und Modal-Texte differenzieren nun
zwischen DSB- und Mandanten-Pruefung.

- WorkflowStatusBar zeigt 5 Schritte: draft -> review_internal ->
  review_client -> approved -> published, mit status-spezifischen
  Action-Buttons (Save/Submit, DSB-Freigabe, Mandant-Freigabe, Publish).
- ApprovalModal differenziert Mode 'approve-internal' / 'approve-client' /
  'reject' mit eigenen Titles und Button-Labels.
- useWorkflowActions ruft neue Endpoints /approve-internal und
  /approve-client (Backend Phase 1); approveVersion bleibt als
  Backward-Compat-Alias.
- page.tsx leitet Modal-Confirm an passende Action weiter und akzeptiert
  review_internal/review_client im draftVersion-Filter.
- _types.ts: Status-Union + STATUS_LABELS um beide Review-Stufen
  erweitert; alter 'review'-Wert bleibt fuer Bestandsdaten erhalten.
- CompareView, SplitViewEditor, HistoryPanel: Status-Rendering und neue
  Action-Labels (submitted_internal, approved_internal, approved_client).

LOC-Exception fuer admin-compliance/lib/sdk/types/sdk-steps.ts (525):
zentrale SDK-Step-Registry mit kanonischer Reihenfolge — splits wuerden
die globale seq-Garantie zerreissen.

[guardrail-change]

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-06-08 10:15:32 +02:00
parent 5c5d676f01
commit 79ce12caf1
9 changed files with 254 additions and 98 deletions
+8
View File
@@ -134,6 +134,14 @@ docs-src/Breakpilot ComplAI Finanzplan.xlsm
# Phase 5+ target for splitting into smaller subcomponents per wizard step. # Phase 5+ target for splitting into smaller subcomponents per wizard step.
admin-compliance/components/sdk/ai-act/DecisionTreeWizard.tsx admin-compliance/components/sdk/ai-act/DecisionTreeWizard.tsx
# --- admin-compliance: zentrale SDK-Schritt-Registry ---
# Flache Liste aller 38 SDK-Steps mit kanonischer Reihenfolge (seq).
# Splits nach Paket würden die globale Ordnungs-Garantie zerreißen und
# Imports an mehreren Stellen aufblähen — der Wert dieser Datei ist
# *eine* sortierte Source-of-Truth.
# [guardrail-change]
admin-compliance/lib/sdk/types/sdk-steps.ts
# --- ai-compliance-sdk: oversized handler refactor backlog --- # --- ai-compliance-sdk: oversized handler refactor backlog ---
# Phase 5+ target for splitting handler groups into per-resource files. # Phase 5+ target for splitting handler groups into per-resource files.
ai-compliance-sdk/internal/api/handlers/tender_handlers.go ai-compliance-sdk/internal/api/handlers/tender_handlers.go
@@ -1,7 +1,9 @@
'use client' 'use client'
export type ApprovalModalMode = 'approve-internal' | 'approve-client' | 'reject'
interface ApprovalModalProps { interface ApprovalModalProps {
mode: 'approve' | 'reject' mode: ApprovalModalMode
approvalComment: string approvalComment: string
onCommentChange: (comment: string) => void onCommentChange: (comment: string) => void
onCancel: () => void onCancel: () => void
@@ -9,6 +11,26 @@ interface ApprovalModalProps {
saving: boolean saving: boolean
} }
const TITLES: Record<ApprovalModalMode, string> = {
'approve-internal': 'DSB-Freigabe → an Mandant weiterleiten',
'approve-client': 'Mandanten-Freigabe erteilen',
reject: 'Version ablehnen',
}
const BUTTON_LABELS: Record<ApprovalModalMode, string> = {
'approve-internal': 'DSB-Freigabe erteilen',
'approve-client': 'Mandanten-Freigabe erteilen',
reject: 'Ablehnen',
}
const PLACEHOLDERS: Record<ApprovalModalMode, string> = {
'approve-internal':
'Kommentar (optional) — Hinweise für den Mandanten...',
'approve-client':
'Kommentar (optional) — z.B. Freigabe durch Geschäftsführung...',
reject: 'Ablehnungsgrund...',
}
export default function ApprovalModal({ export default function ApprovalModal({
mode, mode,
approvalComment, approvalComment,
@@ -17,18 +39,17 @@ export default function ApprovalModal({
onConfirm, onConfirm,
saving, saving,
}: ApprovalModalProps) { }: ApprovalModalProps) {
const isReject = mode === 'reject'
return ( return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"> <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"> <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"> <h3 className="text-lg font-semibold text-slate-900 mb-4">{TITLES[mode]}</h3>
{mode === 'approve' ? 'Version freigeben' : 'Version ablehnen'}
</h3>
<textarea <textarea
value={approvalComment} value={approvalComment}
onChange={(e) => onCommentChange(e.target.value)} onChange={(e) => onCommentChange(e.target.value)}
placeholder={mode === 'approve' ? 'Kommentar (optional)...' : 'Ablehnungsgrund...'} placeholder={PLACEHOLDERS[mode]}
className="w-full px-3 py-2 border border-slate-300 rounded-lg min-h-[100px] mb-4" className="w-full px-3 py-2 border border-slate-300 rounded-lg min-h-[100px] mb-4"
required={mode === 'reject'} required={isReject}
/> />
<div className="flex justify-end gap-3"> <div className="flex justify-end gap-3">
<button <button
@@ -39,14 +60,12 @@ export default function ApprovalModal({
</button> </button>
<button <button
onClick={onConfirm} onClick={onConfirm}
disabled={saving || (mode === 'reject' && !approvalComment)} disabled={saving || (isReject && !approvalComment)}
className={`px-4 py-2 text-white rounded-lg disabled:opacity-50 ${ className={`px-4 py-2 text-white rounded-lg disabled:opacity-50 ${
mode === 'approve' isReject ? 'bg-red-600 hover:bg-red-700' : 'bg-green-600 hover:bg-green-700'
? 'bg-green-600 hover:bg-green-700'
: 'bg-red-600 hover:bg-red-700'
}`} }`}
> >
{saving ? 'Wird verarbeitet...' : mode === 'approve' ? 'Freigeben' : 'Ablehnen'} {saving ? 'Wird verarbeitet...' : BUTTON_LABELS[mode]}
</button> </button>
</div> </div>
</div> </div>
@@ -1,6 +1,7 @@
'use client' 'use client'
import { Version, STATUS_LABELS } from '../_types' import { Version, STATUS_LABELS } from '../_types'
import type { ApprovalModalMode } from './ApprovalModal'
interface CompareViewProps { interface CompareViewProps {
currentVersion: Version | null currentVersion: Version | null
@@ -9,7 +10,7 @@ interface CompareViewProps {
onClose: () => void onClose: () => void
onSaveDraft: () => void onSaveDraft: () => void
onSubmitForReview: () => void onSubmitForReview: () => void
onShowApprovalModal: (mode: 'approve' | 'reject') => void onShowApprovalModal: (mode: ApprovalModalMode) => void
onPublishVersion: () => void onPublishVersion: () => void
} }
@@ -64,28 +65,26 @@ export default function CompareView({
{/* Right: Draft */} {/* Right: Draft */}
<div className="bg-white flex flex-col"> <div className="bg-white flex flex-col">
<div className={`border-b px-4 py-2 ${ <div
draftVersion?.status === 'draft' ? 'bg-yellow-100 border-yellow-200' : className={`border-b px-4 py-2 ${
draftVersion?.status === 'review' ? 'bg-blue-100 border-blue-200' : draftVersion?.status === 'draft'
draftVersion?.status === 'approved' ? 'bg-green-100 border-green-200' : ? 'bg-yellow-100 border-yellow-200'
'bg-slate-100 border-slate-200' : draftVersion?.status === 'review' || draftVersion?.status === 'review_internal'
}`}> ? 'bg-blue-100 border-blue-200'
<span className={`font-medium ${ : draftVersion?.status === 'review_client'
draftVersion?.status === 'draft' ? 'text-yellow-800' : ? 'bg-indigo-100 border-indigo-200'
draftVersion?.status === 'review' ? 'text-blue-800' : : draftVersion?.status === 'approved'
draftVersion?.status === 'approved' ? 'text-green-800' : ? 'bg-green-100 border-green-200'
'text-slate-800' : 'bg-slate-100 border-slate-200'
}`}> }`}
>
<span className="font-medium text-slate-800">
{draftVersion ? 'Aenderungsversion' : 'Neue Version'} {draftVersion ? 'Aenderungsversion' : 'Neue Version'}
</span> </span>
{draftVersion && ( {draftVersion && (
<span className={`ml-2 ${ <span className="ml-2 text-slate-600">
draftVersion.status === 'draft' ? 'text-yellow-600' : v{draftVersion.version} -{' '}
draftVersion.status === 'review' ? 'text-blue-600' : {STATUS_LABELS[draftVersion.status]?.label ?? draftVersion.status}
draftVersion.status === 'approved' ? 'text-green-600' :
'text-slate-600'
}`}>
v{draftVersion.version} - {STATUS_LABELS[draftVersion.status].label}
</span> </span>
)} )}
</div> </div>
@@ -113,7 +112,7 @@ export default function CompareView({
</button> </button>
</> </>
)} )}
{draftVersion?.status === 'review' && ( {draftVersion?.status === 'review_internal' && (
<> <>
<button <button
onClick={() => { onClose(); onShowApprovalModal('reject') }} onClick={() => { onClose(); onShowApprovalModal('reject') }}
@@ -122,10 +121,26 @@ export default function CompareView({
Ablehnen Ablehnen
</button> </button>
<button <button
onClick={() => { onClose(); onShowApprovalModal('approve') }} onClick={() => { onClose(); onShowApprovalModal('approve-internal') }}
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-500" className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-500"
> >
Freigeben DSB-Freigabe Mandant
</button>
</>
)}
{draftVersion?.status === 'review_client' && (
<>
<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-client') }}
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-500"
>
Mandanten-Freigabe
</button> </button>
</> </>
)} )}
@@ -9,6 +9,32 @@ interface HistoryPanelProps {
currentVersion: Version | null currentVersion: Version | null
} }
// Backend-Actions (compliance/services/legal_document_service.py):
// submitted_internal, approved_internal, approved_client,
// published, rejected, plus alte Werte 'submitted'/'approved'.
const ACTION_LABELS: Record<string, string> = {
submitted: 'Eingereicht',
submitted_internal: 'An DSB eingereicht',
approved: 'Freigegeben',
approved_internal: 'DSB-Freigabe → Mandant',
approved_client: 'Mandanten-Freigabe',
published: 'Veroeffentlicht',
rejected: 'Abgelehnt',
}
function actionLabel(action: string): string {
return ACTION_LABELS[action] || action
}
function actionBadgeClass(action: string): string {
if (action.startsWith('approved') || action === 'published') {
return 'bg-green-100 text-green-700'
}
if (action === 'rejected') return 'bg-red-100 text-red-700'
if (action.startsWith('submitted')) return 'bg-blue-100 text-blue-700'
return 'bg-slate-100 text-slate-700'
}
export default function HistoryPanel({ export default function HistoryPanel({
approvalHistory, approvalHistory,
versions, versions,
@@ -22,12 +48,9 @@ export default function HistoryPanel({
<div className="space-y-3"> <div className="space-y-3">
{approvalHistory.map((item, idx) => ( {approvalHistory.map((item, idx) => (
<div key={idx} className="flex items-center gap-4 p-3 border border-slate-200 rounded-lg"> <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 ${ <span className={`px-2 py-1 rounded text-xs ${actionBadgeClass(item.action)}`}>
item.action === 'approved' ? 'bg-green-100 text-green-700' : {actionLabel(item.action)}
item.action === 'rejected' ? 'bg-red-100 text-red-700' : </span>
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> <span className="text-sm text-slate-600">{item.approver || 'System'}</span>
{item.comment && ( {item.comment && (
<span className="text-sm text-slate-500 italic">&quot;{item.comment}&quot;</span> <span className="text-sm text-slate-500 italic">&quot;{item.comment}&quot;</span>
@@ -56,8 +79,12 @@ export default function HistoryPanel({
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span className="font-mono font-medium">v{v.version}</span> <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}`}> <span
{STATUS_LABELS[v.status].label} className={`px-2 py-0.5 rounded text-xs ${
STATUS_LABELS[v.status]?.color ?? 'bg-slate-100 text-slate-700'
}`}
>
{STATUS_LABELS[v.status]?.label ?? v.status}
</span> </span>
<span className="text-sm text-slate-500">{v.title}</span> <span className="text-sm text-slate-500">{v.title}</span>
</div> </div>
@@ -70,29 +70,27 @@ export default function SplitViewEditor({
{/* Right: Draft/Edit Version */} {/* Right: Draft/Edit Version */}
<div className="bg-white rounded-xl shadow-sm border overflow-hidden"> <div className="bg-white rounded-xl shadow-sm border overflow-hidden">
<div className={`border-b px-4 py-3 flex items-center justify-between ${ <div
draftVersion?.status === 'draft' ? 'bg-yellow-50 border-yellow-200' : className={`border-b px-4 py-3 flex items-center justify-between ${
draftVersion?.status === 'review' ? 'bg-blue-50 border-blue-200' : draftVersion?.status === 'draft'
draftVersion?.status === 'approved' ? 'bg-green-50 border-green-200' : ? 'bg-yellow-50 border-yellow-200'
'bg-slate-50 border-slate-200' : draftVersion?.status === 'review' || draftVersion?.status === 'review_internal'
}`}> ? 'bg-blue-50 border-blue-200'
: draftVersion?.status === 'review_client'
? 'bg-indigo-50 border-indigo-200'
: draftVersion?.status === 'approved'
? 'bg-green-50 border-green-200'
: 'bg-slate-50 border-slate-200'
}`}
>
<div> <div>
<h3 className={`font-semibold ${ <h3 className="font-semibold text-slate-900">
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'} {draftVersion ? 'Aenderungsversion' : 'Neue Version'}
</h3> </h3>
{draftVersion && ( {draftVersion && (
<p className={`text-sm ${ <p className="text-sm text-slate-700">
draftVersion.status === 'draft' ? 'text-yellow-700' : v{draftVersion.version} -{' '}
draftVersion.status === 'review' ? 'text-blue-700' : {STATUS_LABELS[draftVersion.status]?.label ?? draftVersion.status}
draftVersion.status === 'approved' ? 'text-green-700' :
'text-slate-700'
}`}>
v{draftVersion.version} - {STATUS_LABELS[draftVersion.status].label}
</p> </p>
)} )}
</div> </div>
@@ -2,16 +2,35 @@
import { Version } from '../_types' import { Version } from '../_types'
export type ApprovalMode = 'approve-internal' | 'approve-client' | 'reject'
interface WorkflowStatusBarProps { interface WorkflowStatusBarProps {
draftVersion: Version | null draftVersion: Version | null
saving: boolean saving: boolean
onCreateNewDraft: () => void onCreateNewDraft: () => void
onSaveDraft: () => void onSaveDraft: () => void
onSubmitForReview: () => void onSubmitForReview: () => void
onShowApprovalModal: (mode: 'approve' | 'reject') => void onShowApprovalModal: (mode: ApprovalMode) => void
onPublishVersion: () => void onPublishVersion: () => void
} }
// 5-Stage Lifecycle:
// draft → review_internal (DSB-Pruefung) → review_client (Mandant-Pruefung)
// → approved → published
// Buttons sind v1 nicht role-gefiltert — alle relevanten Aktionen sichtbar.
const STAGES: { status: string; label: string }[] = [
{ status: 'draft', label: 'Entwurf' },
{ status: 'review_internal', label: 'DSB-Pruefung' },
{ status: 'review_client', label: 'Mandant-Pruefung' },
{ status: 'approved', label: 'Freigegeben' },
{ status: 'published', label: 'Veroeffentlicht' },
]
function isActiveStage(stageStatus: string, draftStatus: string | undefined, hasDraft: boolean) {
if (stageStatus === 'published') return !hasDraft
return draftStatus === stageStatus
}
export default function WorkflowStatusBar({ export default function WorkflowStatusBar({
draftVersion, draftVersion,
saving, saving,
@@ -21,34 +40,31 @@ export default function WorkflowStatusBar({
onShowApprovalModal, onShowApprovalModal,
onPublishVersion, onPublishVersion,
}: WorkflowStatusBarProps) { }: WorkflowStatusBarProps) {
const status = draftVersion?.status
return ( return (
<div className="bg-white rounded-xl shadow-sm border p-4"> <div className="bg-white rounded-xl shadow-sm border p-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between flex-wrap gap-4">
<div className="flex items-center gap-6"> <div className="flex items-center gap-4 flex-wrap">
{['draft', 'review', 'approved', 'published'].map((status, idx) => ( {STAGES.map((stage, idx) => (
<div key={status} className="flex items-center"> <div key={stage.status} className="flex items-center">
{idx > 0 && <div className="w-8 h-0.5 bg-slate-200 mr-2" />} {idx > 0 && <div className="w-6 h-0.5 bg-slate-200 mr-2" />}
<div className="flex items-center gap-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 ${ <div
(status === 'draft' && draftVersion?.status === 'draft') || className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium ${
(status === 'review' && draftVersion?.status === 'review') || isActiveStage(stage.status, status, Boolean(draftVersion))
(status === 'approved' && draftVersion?.status === 'approved') || ? 'bg-purple-500 text-white'
(status === 'published' && !draftVersion) : 'bg-slate-200 text-slate-600'
? 'bg-purple-500 text-white' }`}
: 'bg-slate-200 text-slate-600' >
}`}>{idx + 1}</div> {idx + 1}
<span className="text-sm text-slate-600"> </div>
{status === 'draft' ? 'Entwurf' : <span className="text-sm text-slate-600">{stage.label}</span>
status === 'review' ? 'Pruefung' :
status === 'approved' ? 'Freigegeben' : 'Veroeffentlicht'}
</span>
</div> </div>
</div> </div>
))} ))}
</div> </div>
{/* Action Buttons */} <div className="flex items-center gap-2 flex-wrap">
<div className="flex items-center gap-2">
{!draftVersion && ( {!draftVersion && (
<button <button
onClick={onCreateNewDraft} onClick={onCreateNewDraft}
@@ -59,7 +75,7 @@ export default function WorkflowStatusBar({
</button> </button>
)} )}
{draftVersion?.status === 'draft' && ( {status === 'draft' && (
<> <>
<button <button
onClick={onSaveDraft} onClick={onSaveDraft}
@@ -73,12 +89,12 @@ export default function WorkflowStatusBar({
disabled={saving} 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" 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 An DSB einreichen
</button> </button>
</> </>
)} )}
{draftVersion?.status === 'review' && ( {status === 'review_internal' && (
<> <>
<button <button
onClick={() => onShowApprovalModal('reject')} onClick={() => onShowApprovalModal('reject')}
@@ -88,16 +104,35 @@ export default function WorkflowStatusBar({
Ablehnen Ablehnen
</button> </button>
<button <button
onClick={() => onShowApprovalModal('approve')} onClick={() => onShowApprovalModal('approve-internal')}
disabled={saving} 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" className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50 text-sm font-medium"
> >
Freigeben DSB-Freigabe Mandant
</button> </button>
</> </>
)} )}
{draftVersion?.status === 'approved' && ( {status === 'review_client' && (
<>
<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-client')}
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"
>
Mandant-Freigabe
</button>
</>
)}
{status === 'approved' && (
<button <button
onClick={onPublishVersion} onClick={onPublishVersion}
disabled={saving} disabled={saving}
@@ -107,9 +142,11 @@ export default function WorkflowStatusBar({
</button> </button>
)} )}
{draftVersion?.status === 'rejected' && ( {status === 'rejected' && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-sm text-red-600">Abgelehnt: {draftVersion.rejection_reason}</span> <span className="text-sm text-red-600">
Abgelehnt: {draftVersion?.rejection_reason}
</span>
<button <button
onClick={onCreateNewDraft} onClick={onCreateNewDraft}
disabled={saving} disabled={saving}
@@ -2,6 +2,7 @@
import { useState } from 'react' import { useState } from 'react'
import { Document, Version, ApprovalHistoryItem } from '../_types' import { Document, Version, ApprovalHistoryItem } from '../_types'
import type { ApprovalModalMode } from '../_components/ApprovalModal'
interface UseWorkflowActionsParams { interface UseWorkflowActionsParams {
selectedDocument: Document | null selectedDocument: Document | null
@@ -27,7 +28,7 @@ export function useWorkflowActions(params: UseWorkflowActionsParams) {
const [saving, setSaving] = useState(false) const [saving, setSaving] = useState(false)
const [approvalComment, setApprovalComment] = useState('') const [approvalComment, setApprovalComment] = useState('')
const [showApprovalModal, setShowApprovalModal] = useState<'approve' | 'reject' | null>(null) const [showApprovalModal, setShowApprovalModal] = useState<ApprovalModalMode | null>(null)
const [approvalHistory, setApprovalHistory] = useState<ApprovalHistoryItem[]>([]) const [approvalHistory, setApprovalHistory] = useState<ApprovalHistoryItem[]>([])
const [showNewDocModal, setShowNewDocModal] = useState(false) const [showNewDocModal, setShowNewDocModal] = useState(false)
const [newDocForm, setNewDocForm] = useState({ type: 'privacy_policy', name: '', description: '' }) const [newDocForm, setNewDocForm] = useState({ type: 'privacy_policy', name: '', description: '' })
@@ -123,10 +124,15 @@ export function useWorkflowActions(params: UseWorkflowActionsParams) {
} }
const approveVersion = async () => { const approveVersion = async () => {
// Backward-compat alias — leitet auf approve-internal (DSB → Mandant)
return approveInternal()
}
const approveInternal = async () => {
if (!draftVersion) return if (!draftVersion) return
setSaving(true) setSaving(true)
try { try {
const res = await fetch(`/api/admin/consent/versions/${draftVersion.id}/approve`, { const res = await fetch(`/api/admin/consent/versions/${draftVersion.id}/approve-internal`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ comment: approvalComment }), body: JSON.stringify({ comment: approvalComment }),
@@ -138,10 +144,35 @@ export function useWorkflowActions(params: UseWorkflowActionsParams) {
await loadVersions(selectedDocument!.id) await loadVersions(selectedDocument!.id)
} else { } else {
const err = await res.json() const err = await res.json()
setError(err.error || 'Fehler bei der Freigabe') setError(err.error || 'Fehler bei der DSB-Freigabe')
} }
} catch { } catch {
setError('Fehler bei der Freigabe') setError('Fehler bei der DSB-Freigabe')
} finally {
setSaving(false)
}
}
const approveClient = async () => {
if (!draftVersion) return
setSaving(true)
try {
const res = await fetch(`/api/admin/consent/versions/${draftVersion.id}/approve-client`, {
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 Mandanten-Freigabe')
}
} catch {
setError('Fehler bei der Mandanten-Freigabe')
} finally { } finally {
setSaving(false) setSaving(false)
} }
@@ -242,7 +273,8 @@ export function useWorkflowActions(params: UseWorkflowActionsParams) {
newDocForm, setNewDocForm, newDocForm, setNewDocForm,
creatingDoc, creatingDoc,
createNewDraft, saveDraft, submitForReview, createNewDraft, saveDraft, submitForReview,
approveVersion, rejectVersion, publishVersion, approveVersion, approveInternal, approveClient,
rejectVersion, publishVersion,
createDocument, loadApprovalHistory, createDocument, loadApprovalHistory,
} }
} }
+11 -1
View File
@@ -16,7 +16,15 @@ export interface Version {
title: string title: string
content: string content: string
summary?: string summary?: string
status: 'draft' | 'review' | 'approved' | 'published' | 'archived' | 'rejected' status:
| 'draft'
| 'review' // backward-compat (alte Daten, vor 5-Stage Migration 148)
| 'review_internal'
| 'review_client'
| 'approved'
| 'published'
| 'archived'
| 'rejected'
created_at: string created_at: string
updated_at?: string updated_at?: string
created_by?: string created_by?: string
@@ -35,6 +43,8 @@ export interface ApprovalHistoryItem {
export const STATUS_LABELS: Record<string, { label: string; color: string }> = { export const STATUS_LABELS: Record<string, { label: string; color: string }> = {
draft: { label: 'Entwurf', color: 'bg-yellow-100 text-yellow-700' }, draft: { label: 'Entwurf', color: 'bg-yellow-100 text-yellow-700' },
review: { label: 'In Pruefung', color: 'bg-blue-100 text-blue-700' }, review: { label: 'In Pruefung', color: 'bg-blue-100 text-blue-700' },
review_internal: { label: 'DSB-Pruefung', color: 'bg-blue-100 text-blue-700' },
review_client: { label: 'Mandant-Pruefung', color: 'bg-indigo-100 text-indigo-700' },
approved: { label: 'Freigegeben', color: 'bg-green-100 text-green-700' }, approved: { label: 'Freigegeben', color: 'bg-green-100 text-green-700' },
published: { label: 'Veroeffentlicht', color: 'bg-emerald-100 text-emerald-700' }, published: { label: 'Veroeffentlicht', color: 'bg-emerald-100 text-emerald-700' },
archived: { label: 'Archiviert', color: 'bg-slate-100 text-slate-700' }, archived: { label: 'Archiviert', color: 'bg-slate-100 text-slate-700' },
+12 -2
View File
@@ -92,7 +92,11 @@ export default function WorkflowPage() {
setCurrentVersion(published || null) setCurrentVersion(published || null)
const draft = versionList.find((v: Version) => const draft = versionList.find((v: Version) =>
v.status === 'draft' || v.status === 'review' || v.status === 'approved' v.status === 'draft' ||
v.status === 'review' || // backward-compat: alte Daten
v.status === 'review_internal' ||
v.status === 'review_client' ||
v.status === 'approved'
) )
if (draft) { if (draft) {
setDraftVersion(draft) setDraftVersion(draft)
@@ -256,7 +260,13 @@ export default function WorkflowPage() {
actions.setShowApprovalModal(null) actions.setShowApprovalModal(null)
actions.setApprovalComment('') actions.setApprovalComment('')
}} }}
onConfirm={actions.showApprovalModal === 'approve' ? actions.approveVersion : actions.rejectVersion} onConfirm={
actions.showApprovalModal === 'approve-internal'
? actions.approveInternal
: actions.showApprovalModal === 'approve-client'
? actions.approveClient
: actions.rejectVersion
}
saving={actions.saving} saving={actions.saving}
/> />
)} )}