4c92b17617
- 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>
171 lines
6.4 KiB
TypeScript
171 lines
6.4 KiB
TypeScript
'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>
|
|
)
|
|
}
|