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>
141 lines
5.3 KiB
TypeScript
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>
|
|
)
|
|
}
|