Files
breakpilot-compliance/admin-compliance/app/sdk/rollenkonzept/page.tsx
T
Benjamin Admin 4c92b17617 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>
2026-05-03 13:09:32 +02:00

141 lines
5.3 KiB
TypeScript

'use client'
import { useState } from 'react'
import { useOrgRoles } from './_hooks/useOrgRoles'
import { useDocumentReviews } from './_hooks/useDocumentReviews'
import { RoleCard } from './_components/RoleCard'
import { ReviewList } from './_components/ReviewList'
import type { RollenkonzeptTab } from './_types'
const TABS: { id: RollenkonzeptTab; label: string }[] = [
{ id: 'rollen', label: 'Rollen' },
{ id: 'zuordnung', label: 'Zuordnung' },
{ id: 'reviews', label: 'Reviews' },
]
export default function RollenkonzeptPage() {
const [tab, setTab] = useState<RollenkonzeptTab>('rollen')
const { roles, defaults, mapping, loading, seedRoles, updateRole, sendTestEmail } = useOrgRoles()
const reviewHook = useDocumentReviews()
// Merge defaults with actual roles
const mergedRoles = defaults.map(d => {
const actual = roles.find(r => r.role_key === d.role_key)
return actual || d
})
// Group mapping by role
const mappingByRole: Record<string, string[]> = {}
for (const m of mapping) {
if (!mappingByRole[m.role_key]) mappingByRole[m.role_key] = []
mappingByRole[m.role_key].push(m.document_type)
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-start justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900">Rollenkonzept</h1>
<p className="mt-1 text-gray-500">
Weisen Sie Compliance-Rollen zu und verwalten Sie den Dokumenten-Pruefprozess.
</p>
</div>
{roles.length === 0 && !loading && (
<button onClick={seedRoles}
className="px-4 py-2 bg-purple-600 text-white text-sm font-medium rounded-lg hover:bg-purple-700">
Standard-Rollen anlegen
</button>
)}
</div>
{/* Tabs */}
<div className="flex border-b border-gray-200">
{TABS.map(t => (
<button key={t.id} onClick={() => setTab(t.id)}
className={`px-4 py-3 text-sm font-medium border-b-2 -mb-px transition-colors ${
tab === t.id ? 'text-purple-600 border-purple-600' : 'text-gray-500 border-transparent hover:text-gray-700'
}`}>
{t.label}
{t.id === 'reviews' && (reviewHook.stats.pending || 0) > 0 && (
<span className="ml-2 px-1.5 py-0.5 text-[10px] bg-orange-100 text-orange-600 rounded-full">
{reviewHook.stats.pending}
</span>
)}
</button>
))}
</div>
{/* Tab: Rollen */}
{tab === 'rollen' && (
<div>
{loading ? (
<div className="text-center py-12 text-gray-400">Lade Rollen...</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{mergedRoles.map(role => (
<RoleCard key={role.role_key} role={role} onSave={updateRole} onSendTest={sendTestEmail} />
))}
</div>
)}
</div>
)}
{/* Tab: Zuordnung */}
{tab === 'zuordnung' && (
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
<div className="px-6 py-4 border-b border-gray-100">
<h2 className="font-semibold text-gray-900">Dokument Rollen-Zuordnung</h2>
<p className="text-xs text-gray-500 mt-1">
Zeigt welche Rolle welche Dokumente zur Pruefung erhaelt. Anpassbar pro Tenant.
</p>
</div>
{Object.keys(mappingByRole).length === 0 ? (
<div className="text-center py-8 text-gray-400">
Keine Zuordnung vorhanden. Bitte erst Standard-Rollen anlegen.
</div>
) : (
<div className="divide-y divide-gray-100">
{defaults.map(d => {
const docs = mappingByRole[d.role_key] || []
return (
<div key={d.role_key} className="px-6 py-3">
<div className="flex items-center gap-2 mb-2">
<span className="font-medium text-sm text-gray-900">{d.role_label}</span>
<span className="text-xs text-gray-400">({docs.length} Dokumente)</span>
</div>
<div className="flex flex-wrap gap-1">
{docs.map(dt => (
<span key={dt} className="px-2 py-0.5 text-[10px] bg-gray-100 text-gray-600 rounded-full">
{dt.replace(/_/g, ' ')}
</span>
))}
{docs.length === 0 && (
<span className="text-xs text-gray-400 italic">Keine Dokumente zugeordnet</span>
)}
</div>
</div>
)
})}
</div>
)}
</div>
)}
{/* Tab: Reviews */}
{tab === 'reviews' && (
<ReviewList
reviews={reviewHook.reviews}
stats={reviewHook.stats}
loading={reviewHook.loading}
statusFilter={reviewHook.statusFilter}
onFilterChange={reviewHook.setStatusFilter}
onApprove={reviewHook.approveReview}
onReject={reviewHook.rejectReview}
onSendNotification={reviewHook.sendNotification}
/>
)}
</div>
)
}