feat: Rollenkonzept module + Document Generator review integration (Phase 4-5)
- New /sdk/rollenkonzept/ module with 3 tabs (Rollen, Zuordnung, Reviews) - 7 standard compliance roles (DSB, GF, IT-Leiter, HR, Marketing, Compliance, Einkauf) - Inline role editing with test email via Mailpit - Document-to-role mapping table (editable per tenant) - Review list with status filters and approve/reject workflow - ReviewAssignmentPanel in Document Generator preview tab - "Zur Pruefung senden" button creates reviews + sends notification emails - Approval notification sent to all affected roles after document sign-off - Sidebar navigation link added Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
import { useState } from 'react'
|
||||
import { LegalTemplateResult } from '@/lib/sdk/types'
|
||||
import { RuleEngineResult } from '../ruleEngine'
|
||||
import ReviewAssignmentPanel from './ReviewAssignmentPanel'
|
||||
|
||||
interface GeneratorPreviewTabProps {
|
||||
template: LegalTemplateResult
|
||||
@@ -243,6 +244,13 @@ export default function GeneratorPreviewTab({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Review Assignment */}
|
||||
<ReviewAssignmentPanel
|
||||
documentType={template.templateType || ''}
|
||||
documentTitle={template.documentTitle || 'Dokument'}
|
||||
documentContent={renderedContent}
|
||||
/>
|
||||
|
||||
{/* Attribution */}
|
||||
{template.attributionRequired && template.attributionText && (
|
||||
<div className="text-xs text-orange-600 bg-orange-50 p-3 rounded-lg border border-orange-200">
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useSDK } from '@/lib/sdk'
|
||||
|
||||
interface ReviewerInfo {
|
||||
role_key: string
|
||||
role_label?: string
|
||||
person_name?: string | null
|
||||
person_email?: string | null
|
||||
is_primary?: boolean
|
||||
}
|
||||
|
||||
interface ReviewRecord {
|
||||
id: string
|
||||
status: string
|
||||
reviewer_role_key: string
|
||||
reviewer_name: string | null
|
||||
email_sent: boolean
|
||||
}
|
||||
|
||||
const STATUS_COLORS: Record<string, string> = {
|
||||
pending: 'bg-gray-100 text-gray-700',
|
||||
in_review: 'bg-blue-100 text-blue-700',
|
||||
approved: 'bg-green-100 text-green-700',
|
||||
rejected: 'bg-red-100 text-red-700',
|
||||
}
|
||||
|
||||
const STATUS_LABELS: Record<string, string> = {
|
||||
pending: 'Ausstehend',
|
||||
in_review: 'In Pruefung',
|
||||
approved: 'Freigegeben',
|
||||
rejected: 'Abgelehnt',
|
||||
}
|
||||
|
||||
export default function ReviewAssignmentPanel({
|
||||
documentType,
|
||||
documentTitle,
|
||||
documentContent,
|
||||
}: {
|
||||
documentType: string
|
||||
documentTitle: string
|
||||
documentContent: string
|
||||
}) {
|
||||
const { projectId } = useSDK()
|
||||
const [reviewers, setReviewers] = useState<ReviewerInfo[]>([])
|
||||
const [existingReviews, setExistingReviews] = useState<ReviewRecord[]>([])
|
||||
const [sending, setSending] = useState(false)
|
||||
const [result, setResult] = useState<string | null>(null)
|
||||
|
||||
// Load reviewers for this document type
|
||||
useEffect(() => {
|
||||
if (!documentType) return
|
||||
const qs = new URLSearchParams()
|
||||
if (projectId) qs.set('project_id', projectId)
|
||||
qs.set('document_type', documentType)
|
||||
|
||||
// Load mapping + existing reviews
|
||||
Promise.all([
|
||||
fetch(`/api/sdk/v1/compliance/org-roles/mapping`).then(r => r.ok ? r.json() : []),
|
||||
fetch(`/api/sdk/v1/compliance/org-roles${projectId ? `?project_id=${projectId}` : ''}`).then(r => r.ok ? r.json() : []),
|
||||
fetch(`/api/sdk/v1/compliance/document-reviews/for-document?${qs}`).then(r => r.ok ? r.json() : []),
|
||||
]).then(([mappings, roles, reviews]) => {
|
||||
// Filter mappings for this document type
|
||||
const relevant = (mappings as Array<{ document_type: string; role_key: string; is_primary: boolean }>)
|
||||
.filter(m => m.document_type === documentType)
|
||||
// Enrich with role info
|
||||
const enriched: ReviewerInfo[] = relevant.map(m => {
|
||||
const role = (roles as Array<{ role_key: string; role_label: string; person_name: string | null; person_email: string | null }>)
|
||||
.find(r => r.role_key === m.role_key)
|
||||
return { ...m, role_label: role?.role_label, person_name: role?.person_name, person_email: role?.person_email }
|
||||
})
|
||||
setReviewers(enriched)
|
||||
setExistingReviews(reviews)
|
||||
}).catch(() => {})
|
||||
}, [documentType, projectId])
|
||||
|
||||
const handleSendForReview = async () => {
|
||||
setSending(true)
|
||||
setResult(null)
|
||||
try {
|
||||
const res = await fetch('/api/sdk/v1/compliance/document-reviews', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
document_type: documentType,
|
||||
document_title: documentTitle,
|
||||
document_content: documentContent,
|
||||
project_id: projectId,
|
||||
review_link: window.location.href,
|
||||
}),
|
||||
})
|
||||
if (!res.ok) throw new Error('Fehler beim Erstellen')
|
||||
const reviews = await res.json()
|
||||
|
||||
// Send email for each review
|
||||
let sentCount = 0
|
||||
for (const review of reviews) {
|
||||
if (review.reviewer_email) {
|
||||
const sendRes = await fetch(`/api/sdk/v1/compliance/document-reviews/${review.id}/send`, { method: 'POST' })
|
||||
if (sendRes.ok) sentCount++
|
||||
}
|
||||
}
|
||||
setResult(`${reviews.length} Review(s) erstellt, ${sentCount} E-Mail(s) gesendet`)
|
||||
// Refresh
|
||||
const qs = new URLSearchParams({ document_type: documentType })
|
||||
if (projectId) qs.set('project_id', projectId)
|
||||
const updated = await fetch(`/api/sdk/v1/compliance/document-reviews/for-document?${qs}`).then(r => r.json())
|
||||
setExistingReviews(updated)
|
||||
} catch (e) {
|
||||
setResult(e instanceof Error ? e.message : 'Fehler')
|
||||
} finally {
|
||||
setSending(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (reviewers.length === 0 && existingReviews.length === 0) return null
|
||||
|
||||
return (
|
||||
<div className="border border-purple-200 rounded-lg p-4 bg-purple-50/50 space-y-3">
|
||||
<h4 className="font-semibold text-sm text-gray-900 flex items-center gap-2">
|
||||
<svg className="w-4 h-4 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
Pruefung & Freigabe
|
||||
</h4>
|
||||
|
||||
{/* Assigned reviewers */}
|
||||
{reviewers.length > 0 && (
|
||||
<div className="space-y-1">
|
||||
{reviewers.map(r => (
|
||||
<div key={r.role_key} className="flex items-center gap-2 text-xs">
|
||||
<span className="font-medium text-gray-700">{r.role_label || r.role_key}:</span>
|
||||
{r.person_name ? (
|
||||
<span className="text-gray-600">{r.person_name} ({r.person_email || 'keine E-Mail'})</span>
|
||||
) : (
|
||||
<span className="text-gray-400 italic">Nicht zugewiesen</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Existing reviews */}
|
||||
{existingReviews.length > 0 && (
|
||||
<div className="space-y-1">
|
||||
{existingReviews.map(r => (
|
||||
<div key={r.id} className="flex items-center gap-2">
|
||||
<span className={`px-2 py-0.5 text-[10px] font-medium rounded-full ${STATUS_COLORS[r.status] || ''}`}>
|
||||
{STATUS_LABELS[r.status] || r.status}
|
||||
</span>
|
||||
<span className="text-xs text-gray-600">{r.reviewer_name || r.reviewer_role_key}</span>
|
||||
{r.email_sent && <span className="text-[10px] text-green-600">E-Mail gesendet</span>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Send for review */}
|
||||
<button onClick={handleSendForReview} disabled={sending || reviewers.length === 0}
|
||||
className="w-full px-4 py-2 text-sm bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 transition-colors">
|
||||
{sending ? 'Sende...' : 'Zur Pruefung senden'}
|
||||
</button>
|
||||
|
||||
{result && (
|
||||
<p className={`text-xs ${result.includes('Fehler') ? 'text-red-600' : 'text-green-600'}`}>{result}</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user