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
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:
@@ -134,6 +134,14 @@ docs-src/Breakpilot ComplAI Finanzplan.xlsm
|
||||
# Phase 5+ target for splitting into smaller subcomponents per wizard step.
|
||||
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 ---
|
||||
# Phase 5+ target for splitting handler groups into per-resource files.
|
||||
ai-compliance-sdk/internal/api/handlers/tender_handlers.go
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
'use client'
|
||||
|
||||
export type ApprovalModalMode = 'approve-internal' | 'approve-client' | 'reject'
|
||||
|
||||
interface ApprovalModalProps {
|
||||
mode: 'approve' | 'reject'
|
||||
mode: ApprovalModalMode
|
||||
approvalComment: string
|
||||
onCommentChange: (comment: string) => void
|
||||
onCancel: () => void
|
||||
@@ -9,6 +11,26 @@ interface ApprovalModalProps {
|
||||
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({
|
||||
mode,
|
||||
approvalComment,
|
||||
@@ -17,18 +39,17 @@ export default function ApprovalModal({
|
||||
onConfirm,
|
||||
saving,
|
||||
}: ApprovalModalProps) {
|
||||
const isReject = mode === 'reject'
|
||||
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>
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">{TITLES[mode]}</h3>
|
||||
<textarea
|
||||
value={approvalComment}
|
||||
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"
|
||||
required={mode === 'reject'}
|
||||
required={isReject}
|
||||
/>
|
||||
<div className="flex justify-end gap-3">
|
||||
<button
|
||||
@@ -39,14 +60,12 @@ export default function ApprovalModal({
|
||||
</button>
|
||||
<button
|
||||
onClick={onConfirm}
|
||||
disabled={saving || (mode === 'reject' && !approvalComment)}
|
||||
disabled={saving || (isReject && !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'
|
||||
isReject ? 'bg-red-600 hover:bg-red-700' : 'bg-green-600 hover:bg-green-700'
|
||||
}`}
|
||||
>
|
||||
{saving ? 'Wird verarbeitet...' : mode === 'approve' ? 'Freigeben' : 'Ablehnen'}
|
||||
{saving ? 'Wird verarbeitet...' : BUTTON_LABELS[mode]}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { Version, STATUS_LABELS } from '../_types'
|
||||
import type { ApprovalModalMode } from './ApprovalModal'
|
||||
|
||||
interface CompareViewProps {
|
||||
currentVersion: Version | null
|
||||
@@ -9,7 +10,7 @@ interface CompareViewProps {
|
||||
onClose: () => void
|
||||
onSaveDraft: () => void
|
||||
onSubmitForReview: () => void
|
||||
onShowApprovalModal: (mode: 'approve' | 'reject') => void
|
||||
onShowApprovalModal: (mode: ApprovalModalMode) => void
|
||||
onPublishVersion: () => void
|
||||
}
|
||||
|
||||
@@ -64,28 +65,26 @@ export default function CompareView({
|
||||
|
||||
{/* 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'
|
||||
}`}>
|
||||
<div
|
||||
className={`border-b px-4 py-2 ${
|
||||
draftVersion?.status === 'draft'
|
||||
? 'bg-yellow-100 border-yellow-200'
|
||||
: draftVersion?.status === 'review' || draftVersion?.status === 'review_internal'
|
||||
? 'bg-blue-100 border-blue-200'
|
||||
: draftVersion?.status === 'review_client'
|
||||
? 'bg-indigo-100 border-indigo-200'
|
||||
: draftVersion?.status === 'approved'
|
||||
? 'bg-green-100 border-green-200'
|
||||
: 'bg-slate-100 border-slate-200'
|
||||
}`}
|
||||
>
|
||||
<span className="font-medium 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 className="ml-2 text-slate-600">
|
||||
v{draftVersion.version} -{' '}
|
||||
{STATUS_LABELS[draftVersion.status]?.label ?? draftVersion.status}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -113,7 +112,7 @@ export default function CompareView({
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{draftVersion?.status === 'review' && (
|
||||
{draftVersion?.status === 'review_internal' && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => { onClose(); onShowApprovalModal('reject') }}
|
||||
@@ -122,10 +121,26 @@ export default function CompareView({
|
||||
Ablehnen
|
||||
</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"
|
||||
>
|
||||
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>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -9,6 +9,32 @@ interface HistoryPanelProps {
|
||||
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({
|
||||
approvalHistory,
|
||||
versions,
|
||||
@@ -22,12 +48,9 @@ export default function HistoryPanel({
|
||||
<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={`px-2 py-1 rounded text-xs ${actionBadgeClass(item.action)}`}>
|
||||
{actionLabel(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>
|
||||
@@ -56,8 +79,12 @@ export default function HistoryPanel({
|
||||
<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
|
||||
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 className="text-sm text-slate-500">{v.title}</span>
|
||||
</div>
|
||||
|
||||
@@ -70,29 +70,27 @@ export default function SplitViewEditor({
|
||||
|
||||
{/* 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
|
||||
className={`border-b px-4 py-3 flex items-center justify-between ${
|
||||
draftVersion?.status === 'draft'
|
||||
? 'bg-yellow-50 border-yellow-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>
|
||||
<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'
|
||||
}`}>
|
||||
<h3 className="font-semibold 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 className="text-sm text-slate-700">
|
||||
v{draftVersion.version} -{' '}
|
||||
{STATUS_LABELS[draftVersion.status]?.label ?? draftVersion.status}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -2,16 +2,35 @@
|
||||
|
||||
import { Version } from '../_types'
|
||||
|
||||
export type ApprovalMode = 'approve-internal' | 'approve-client' | 'reject'
|
||||
|
||||
interface WorkflowStatusBarProps {
|
||||
draftVersion: Version | null
|
||||
saving: boolean
|
||||
onCreateNewDraft: () => void
|
||||
onSaveDraft: () => void
|
||||
onSubmitForReview: () => void
|
||||
onShowApprovalModal: (mode: 'approve' | 'reject') => void
|
||||
onShowApprovalModal: (mode: ApprovalMode) => 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({
|
||||
draftVersion,
|
||||
saving,
|
||||
@@ -21,34 +40,31 @@ export default function WorkflowStatusBar({
|
||||
onShowApprovalModal,
|
||||
onPublishVersion,
|
||||
}: WorkflowStatusBarProps) {
|
||||
const status = draftVersion?.status
|
||||
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 justify-between flex-wrap gap-4">
|
||||
<div className="flex items-center gap-4 flex-wrap">
|
||||
{STAGES.map((stage, idx) => (
|
||||
<div key={stage.status} className="flex items-center">
|
||||
{idx > 0 && <div className="w-6 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
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium ${
|
||||
isActiveStage(stage.status, status, Boolean(draftVersion))
|
||||
? 'bg-purple-500 text-white'
|
||||
: 'bg-slate-200 text-slate-600'
|
||||
}`}
|
||||
>
|
||||
{idx + 1}
|
||||
</div>
|
||||
<span className="text-sm text-slate-600">{stage.label}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{!draftVersion && (
|
||||
<button
|
||||
onClick={onCreateNewDraft}
|
||||
@@ -59,7 +75,7 @@ export default function WorkflowStatusBar({
|
||||
</button>
|
||||
)}
|
||||
|
||||
{draftVersion?.status === 'draft' && (
|
||||
{status === 'draft' && (
|
||||
<>
|
||||
<button
|
||||
onClick={onSaveDraft}
|
||||
@@ -73,12 +89,12 @@ export default function WorkflowStatusBar({
|
||||
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
|
||||
An DSB einreichen
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{draftVersion?.status === 'review' && (
|
||||
{status === 'review_internal' && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => onShowApprovalModal('reject')}
|
||||
@@ -88,16 +104,35 @@ export default function WorkflowStatusBar({
|
||||
Ablehnen
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onShowApprovalModal('approve')}
|
||||
onClick={() => onShowApprovalModal('approve-internal')}
|
||||
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
|
||||
DSB-Freigabe → Mandant
|
||||
</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
|
||||
onClick={onPublishVersion}
|
||||
disabled={saving}
|
||||
@@ -107,9 +142,11 @@ export default function WorkflowStatusBar({
|
||||
</button>
|
||||
)}
|
||||
|
||||
{draftVersion?.status === 'rejected' && (
|
||||
{status === 'rejected' && (
|
||||
<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
|
||||
onClick={onCreateNewDraft}
|
||||
disabled={saving}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Document, Version, ApprovalHistoryItem } from '../_types'
|
||||
import type { ApprovalModalMode } from '../_components/ApprovalModal'
|
||||
|
||||
interface UseWorkflowActionsParams {
|
||||
selectedDocument: Document | null
|
||||
@@ -27,7 +28,7 @@ export function useWorkflowActions(params: UseWorkflowActionsParams) {
|
||||
|
||||
const [saving, setSaving] = useState(false)
|
||||
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 [showNewDocModal, setShowNewDocModal] = useState(false)
|
||||
const [newDocForm, setNewDocForm] = useState({ type: 'privacy_policy', name: '', description: '' })
|
||||
@@ -123,10 +124,15 @@ export function useWorkflowActions(params: UseWorkflowActionsParams) {
|
||||
}
|
||||
|
||||
const approveVersion = async () => {
|
||||
// Backward-compat alias — leitet auf approve-internal (DSB → Mandant)
|
||||
return approveInternal()
|
||||
}
|
||||
|
||||
const approveInternal = async () => {
|
||||
if (!draftVersion) return
|
||||
setSaving(true)
|
||||
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',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ comment: approvalComment }),
|
||||
@@ -138,10 +144,35 @@ export function useWorkflowActions(params: UseWorkflowActionsParams) {
|
||||
await loadVersions(selectedDocument!.id)
|
||||
} else {
|
||||
const err = await res.json()
|
||||
setError(err.error || 'Fehler bei der Freigabe')
|
||||
setError(err.error || 'Fehler bei der DSB-Freigabe')
|
||||
}
|
||||
} 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 {
|
||||
setSaving(false)
|
||||
}
|
||||
@@ -242,7 +273,8 @@ export function useWorkflowActions(params: UseWorkflowActionsParams) {
|
||||
newDocForm, setNewDocForm,
|
||||
creatingDoc,
|
||||
createNewDraft, saveDraft, submitForReview,
|
||||
approveVersion, rejectVersion, publishVersion,
|
||||
approveVersion, approveInternal, approveClient,
|
||||
rejectVersion, publishVersion,
|
||||
createDocument, loadApprovalHistory,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,15 @@ export interface Version {
|
||||
title: string
|
||||
content: 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
|
||||
updated_at?: string
|
||||
created_by?: string
|
||||
@@ -35,6 +43,8 @@ export interface ApprovalHistoryItem {
|
||||
export const STATUS_LABELS: Record<string, { label: string; color: string }> = {
|
||||
draft: { label: 'Entwurf', color: 'bg-yellow-100 text-yellow-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' },
|
||||
published: { label: 'Veroeffentlicht', color: 'bg-emerald-100 text-emerald-700' },
|
||||
archived: { label: 'Archiviert', color: 'bg-slate-100 text-slate-700' },
|
||||
|
||||
@@ -92,7 +92,11 @@ export default function WorkflowPage() {
|
||||
setCurrentVersion(published || null)
|
||||
|
||||
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) {
|
||||
setDraftVersion(draft)
|
||||
@@ -256,7 +260,13 @@ export default function WorkflowPage() {
|
||||
actions.setShowApprovalModal(null)
|
||||
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}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user