SDK modules added/enhanced: - compliance-hub, compliance-scope, consent-management, notfallplan - audit-report, workflow, source-policy, dsms - advisory-board documentation section - TOM dashboard components, TOM generator SDM mapping - DSFA: mitigation library, risk catalog, threshold analysis, source attribution - VVT: baseline catalog, profiling engine, types - Loeschfristen: baseline catalog, compliance engine, export, profiling, types - Compliance scope: engine, profiling, golden tests, types Existing SDK pages updated: - dsfa/[id], tom, vvt, loeschfristen, advisory-board — expanded functionality - SDKSidebar, StepHeader — new navigation items and layout - SDK layout, context, types — expanded type system Other admin-v2 changes: - AI agents page, RAG pipeline DSFA integration - GridOverlay component updates - Companion feature (development + education) - Compliance advisor SOUL definition Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
268 lines
11 KiB
TypeScript
268 lines
11 KiB
TypeScript
'use client'
|
|
import React from 'react'
|
|
import type { ComplianceScopeState, ScopeDecision, ComplianceDepthLevel } from '@/lib/sdk/compliance-scope-types'
|
|
import { DEPTH_LEVEL_LABELS, DEPTH_LEVEL_DESCRIPTIONS, DEPTH_LEVEL_COLORS, DOCUMENT_TYPE_LABELS } from '@/lib/sdk/compliance-scope-types'
|
|
|
|
interface ScopeOverviewTabProps {
|
|
scopeState: ComplianceScopeState
|
|
onStartProfiling: () => void
|
|
onRefreshDecision: () => void
|
|
}
|
|
|
|
export function ScopeOverviewTab({ scopeState, onStartProfiling, onRefreshDecision }: ScopeOverviewTabProps) {
|
|
const { decision, answers } = scopeState
|
|
const hasAnswers = answers && answers.length > 0
|
|
|
|
const getScoreColor = (score: number): string => {
|
|
if (score >= 80) return 'from-red-500 to-red-600'
|
|
if (score >= 60) return 'from-orange-500 to-orange-600'
|
|
if (score >= 40) return 'from-yellow-500 to-yellow-600'
|
|
return 'from-green-500 to-green-600'
|
|
}
|
|
|
|
const getScoreColorBg = (score: number): string => {
|
|
if (score >= 80) return 'bg-red-100'
|
|
if (score >= 60) return 'bg-orange-100'
|
|
if (score >= 40) return 'bg-yellow-100'
|
|
return 'bg-green-100'
|
|
}
|
|
|
|
const renderScoreGauge = (label: string, score: number | undefined) => {
|
|
const value = score ?? 0
|
|
return (
|
|
<div className="space-y-2">
|
|
<div className="flex justify-between items-center">
|
|
<span className="text-sm font-medium text-gray-700">{label}</span>
|
|
<span className="text-sm font-bold text-gray-900">{value}/100</span>
|
|
</div>
|
|
<div className="w-full bg-gray-200 rounded-full h-3 overflow-hidden">
|
|
<div
|
|
className={`h-full bg-gradient-to-r ${getScoreColor(value)} transition-all duration-500`}
|
|
style={{ width: `${value}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const renderLevelBadge = () => {
|
|
if (!decision?.level) {
|
|
return (
|
|
<div className="bg-gray-100 border border-gray-300 rounded-xl p-8 text-center">
|
|
<div className="inline-flex items-center justify-center w-24 h-24 bg-gray-200 rounded-full mb-4">
|
|
<span className="text-4xl font-bold text-gray-400">?</span>
|
|
</div>
|
|
<h3 className="text-xl font-semibold text-gray-600 mb-2">Noch nicht bewertet</h3>
|
|
<p className="text-gray-500">
|
|
Führen Sie das Scope-Profiling durch, um Ihre Compliance-Tiefe zu bestimmen.
|
|
</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const levelColors = DEPTH_LEVEL_COLORS[decision.level]
|
|
return (
|
|
<div className={`${levelColors.bg} border-2 ${levelColors.border} rounded-xl p-8 text-center`}>
|
|
<div className={`inline-flex items-center justify-center w-24 h-24 ${levelColors.badge} rounded-full mb-4`}>
|
|
<span className={`text-4xl font-bold ${levelColors.text}`}>{decision.level}</span>
|
|
</div>
|
|
<h3 className={`text-xl font-semibold ${levelColors.text} mb-2`}>
|
|
{DEPTH_LEVEL_LABELS[decision.level]}
|
|
</h3>
|
|
<p className="text-gray-600">{DEPTH_LEVEL_DESCRIPTIONS[decision.level]}</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const renderActiveHardTriggers = () => {
|
|
if (!decision?.hardTriggers || decision.hardTriggers.length === 0) {
|
|
return null
|
|
}
|
|
|
|
const activeHardTriggers = decision.hardTriggers.filter((ht) => ht.matched)
|
|
|
|
if (activeHardTriggers.length === 0) {
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
|
<div className="flex items-center gap-2 mb-4">
|
|
<svg className="w-5 h-5 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
|
/>
|
|
</svg>
|
|
<h3 className="text-lg font-semibold text-gray-900">Aktive Hard-Trigger</h3>
|
|
</div>
|
|
<div className="space-y-3">
|
|
{activeHardTriggers.map((trigger, idx) => (
|
|
<div
|
|
key={idx}
|
|
className="border-l-4 border-red-500 bg-red-50 rounded-r-lg p-4"
|
|
>
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex-1">
|
|
<h4 className="font-semibold text-gray-900">{trigger.label}</h4>
|
|
<p className="text-sm text-gray-600 mt-1">{trigger.description}</p>
|
|
{trigger.legalReference && (
|
|
<p className="text-xs text-gray-500 mt-2">
|
|
<span className="font-medium">Rechtsgrundlage:</span> {trigger.legalReference}
|
|
</p>
|
|
)}
|
|
{trigger.matchedValue && (
|
|
<p className="text-xs text-gray-700 mt-1">
|
|
<span className="font-medium">Erfasster Wert:</span> {trigger.matchedValue}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const renderDocumentSummary = () => {
|
|
if (!decision?.requiredDocuments) {
|
|
return null
|
|
}
|
|
|
|
const mandatoryDocs = decision.requiredDocuments.filter((doc) => doc.isMandatory)
|
|
const optionalDocs = decision.requiredDocuments.filter((doc) => !doc.isMandatory)
|
|
const totalEffortDays = decision.requiredDocuments.reduce(
|
|
(sum, doc) => sum + (doc.effortEstimate?.days ?? 0),
|
|
0
|
|
)
|
|
|
|
return (
|
|
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">Dokumenten-Übersicht</h3>
|
|
<div className="grid grid-cols-3 gap-4">
|
|
<div className="text-center">
|
|
<div className="text-3xl font-bold text-purple-600">{mandatoryDocs.length}</div>
|
|
<div className="text-sm text-gray-600 mt-1">Pflichtdokumente</div>
|
|
</div>
|
|
<div className="text-center">
|
|
<div className="text-3xl font-bold text-blue-600">{optionalDocs.length}</div>
|
|
<div className="text-sm text-gray-600 mt-1">Optional</div>
|
|
</div>
|
|
<div className="text-center">
|
|
<div className="text-3xl font-bold text-gray-900">{totalEffortDays}</div>
|
|
<div className="text-sm text-gray-600 mt-1">Tage Aufwand (geschätzt)</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const renderRiskFlagsSummary = () => {
|
|
if (!decision?.riskFlags || decision.riskFlags.length === 0) {
|
|
return null
|
|
}
|
|
|
|
const critical = decision.riskFlags.filter((rf) => rf.severity === 'critical').length
|
|
const high = decision.riskFlags.filter((rf) => rf.severity === 'high').length
|
|
const medium = decision.riskFlags.filter((rf) => rf.severity === 'medium').length
|
|
|
|
return (
|
|
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
|
<div className="flex items-center gap-2 mb-4">
|
|
<svg className="w-5 h-5 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
|
/>
|
|
</svg>
|
|
<h3 className="text-lg font-semibold text-gray-900">Risiko-Flags</h3>
|
|
</div>
|
|
<div className="flex gap-6">
|
|
{critical > 0 && (
|
|
<div className="flex items-center gap-2">
|
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
|
|
Kritisch
|
|
</span>
|
|
<span className="text-lg font-bold text-red-600">{critical}</span>
|
|
</div>
|
|
)}
|
|
{high > 0 && (
|
|
<div className="flex items-center gap-2">
|
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-orange-100 text-orange-800">
|
|
Hoch
|
|
</span>
|
|
<span className="text-lg font-bold text-orange-600">{high}</span>
|
|
</div>
|
|
)}
|
|
{medium > 0 && (
|
|
<div className="flex items-center gap-2">
|
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
|
|
Mittel
|
|
</span>
|
|
<span className="text-lg font-bold text-yellow-600">{medium}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Level Badge */}
|
|
{renderLevelBadge()}
|
|
|
|
{/* Scores Section */}
|
|
{decision && (
|
|
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">Score-Übersicht</h3>
|
|
<div className="space-y-4">
|
|
{renderScoreGauge('Risiko-Score', decision.scores?.riskScore)}
|
|
{renderScoreGauge('Komplexitäts-Score', decision.scores?.complexityScore)}
|
|
{renderScoreGauge('Assurance-Score', decision.scores?.assuranceScore)}
|
|
<div className="pt-4 border-t border-gray-200">
|
|
{renderScoreGauge('Gesamt-Score', decision.scores?.compositeScore)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Active Hard Triggers */}
|
|
{renderActiveHardTriggers()}
|
|
|
|
{/* Document Summary */}
|
|
{renderDocumentSummary()}
|
|
|
|
{/* Risk Flags Summary */}
|
|
{renderRiskFlagsSummary()}
|
|
|
|
{/* CTA Section */}
|
|
<div className="bg-gradient-to-r from-purple-50 to-blue-50 rounded-xl border border-purple-200 p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
|
{!hasAnswers ? 'Bereit für das Scope-Profiling?' : 'Ergebnis aktualisieren'}
|
|
</h3>
|
|
<p className="text-gray-600">
|
|
{!hasAnswers
|
|
? 'Beantworten Sie einige Fragen zu Ihrem Unternehmen und erhalten Sie eine präzise Compliance-Bewertung.'
|
|
: 'Haben sich Ihre Unternehmensparameter geändert? Aktualisieren Sie Ihre Bewertung.'}
|
|
</p>
|
|
</div>
|
|
<button
|
|
onClick={!hasAnswers ? onStartProfiling : onRefreshDecision}
|
|
className="px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors font-medium whitespace-nowrap"
|
|
>
|
|
{!hasAnswers ? 'Scope-Profiling starten' : 'Ergebnis aktualisieren'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|