Files
breakpilot-compliance/admin-compliance/components/sdk/compliance-scope/ScopeDecisionTab.tsx
Benjamin Admin 24afed69c1
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 35s
CI / test-python-backend-compliance (push) Successful in 39s
CI / test-python-document-crawler (push) Successful in 27s
CI / test-python-dsms-gateway (push) Successful in 25s
Fix Scope evaluation crash: align property names between engine, types, and components
The engine used short property names (risk, complexity, assurance, composite) while
the ComplianceScores interface defined (risk_score, complexity_score, assurance_need,
composite_score). Components used yet another convention (riskScore, level, hardTriggers).
The main crash was DEPTH_LEVEL_COLORS[decision.level] where decision.level was undefined
(correct property: decision.determinedLevel).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 16:47:01 +01:00

512 lines
23 KiB
TypeScript

'use client'
import React, { useState } from 'react'
import type { ScopeDecision, ComplianceDepthLevel, ApplicableRegulation, SupervisoryAuthorityInfo } 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 ScopeDecisionTabProps {
decision: ScopeDecision | null
answers?: unknown[]
onBackToWizard?: () => void
onGoToExport?: () => void
canEvaluate?: boolean
onEvaluate?: () => void
isEvaluating?: boolean
applicableRegulations?: ApplicableRegulation[]
supervisoryAuthorities?: SupervisoryAuthorityInfo[]
regulationAssessmentLoading?: boolean
onGoToObligations?: () => void
}
export function ScopeDecisionTab({
decision,
onBackToWizard,
onGoToExport,
canEvaluate,
onEvaluate,
isEvaluating,
applicableRegulations,
supervisoryAuthorities,
regulationAssessmentLoading,
onGoToObligations,
}: ScopeDecisionTabProps) {
const [expandedTrigger, setExpandedTrigger] = useState<number | null>(null)
const [showAuditTrail, setShowAuditTrail] = useState(false)
if (!decision) {
return (
<div className="bg-white rounded-xl border border-gray-200 p-12 text-center">
<div className="inline-flex items-center justify-center w-16 h-16 bg-gray-100 rounded-full mb-4">
<svg className="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-2">Keine Entscheidung vorhanden</h3>
<p className="text-gray-600">Bitte führen Sie zuerst das Scope-Profiling durch.</p>
</div>
)
}
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 getSeverityBadge = (severity: 'low' | 'medium' | 'high' | 'critical') => {
const colors = {
low: 'bg-gray-100 text-gray-800',
medium: 'bg-yellow-100 text-yellow-800',
high: 'bg-orange-100 text-orange-800',
critical: 'bg-red-100 text-red-800',
}
const labels = {
low: 'Niedrig',
medium: 'Mittel',
high: 'Hoch',
critical: 'Kritisch',
}
return (
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${colors[severity]}`}>
{labels[severity]}
</span>
)
}
const renderScoreBar = (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>
)
}
return (
<div className="space-y-6">
{/* Level Determination */}
<div className={`${DEPTH_LEVEL_COLORS[decision.determinedLevel].bg} border-2 ${DEPTH_LEVEL_COLORS[decision.determinedLevel].border} rounded-xl p-6`}>
<div className="flex items-start gap-6">
<div className={`flex-shrink-0 w-20 h-20 ${DEPTH_LEVEL_COLORS[decision.determinedLevel].badge} rounded-xl flex items-center justify-center`}>
<span className={`text-3xl font-bold ${DEPTH_LEVEL_COLORS[decision.determinedLevel].text}`}>
{decision.determinedLevel}
</span>
</div>
<div className="flex-1">
<h2 className={`text-2xl font-bold ${DEPTH_LEVEL_COLORS[decision.determinedLevel].text} mb-2`}>
{DEPTH_LEVEL_LABELS[decision.determinedLevel]}
</h2>
<p className="text-gray-700 mb-3">{DEPTH_LEVEL_DESCRIPTIONS[decision.determinedLevel]}</p>
{decision.reasoning && (
<p className="text-sm text-gray-600 italic">{decision.reasoning}</p>
)}
</div>
</div>
</div>
{/* Score Breakdown */}
{decision.scores && (
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Score-Analyse</h3>
<div className="space-y-4">
{renderScoreBar('Risiko-Score', decision.scores.risk_score)}
{renderScoreBar('Komplexitäts-Score', decision.scores.complexity_score)}
{renderScoreBar('Assurance-Score', decision.scores.assurance_need)}
<div className="pt-4 border-t border-gray-200">
{renderScoreBar('Gesamt-Score', decision.scores.composite_score)}
</div>
</div>
</div>
)}
{/* Applicable Regulations */}
{(applicableRegulations || regulationAssessmentLoading) && (
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Anwendbare Regulierungen</h3>
{regulationAssessmentLoading ? (
<div className="flex items-center gap-3 text-gray-500">
<svg className="animate-spin h-5 w-5" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
<span>Regulierungen werden geprueft...</span>
</div>
) : applicableRegulations && applicableRegulations.length > 0 ? (
<div className="space-y-3">
{applicableRegulations.map((reg) => (
<div
key={reg.id}
className="flex items-center justify-between border border-gray-200 rounded-lg p-4 hover:bg-gray-50 transition-colors"
>
<div className="flex items-center gap-3">
<div className="flex-shrink-0 w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
<svg className="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
<div>
<span className="font-semibold text-gray-900">{reg.name}</span>
{reg.classification && (
<span className="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-purple-100 text-purple-800">
{reg.classification}
</span>
)}
</div>
</div>
<div className="text-right text-sm text-gray-600">
<span>{reg.obligation_count} Pflichten</span>
{reg.control_count > 0 && (
<span className="ml-2">{reg.control_count} Controls</span>
)}
</div>
</div>
))}
{/* Supervisory Authorities */}
{supervisoryAuthorities && supervisoryAuthorities.length > 0 && (
<div className="mt-4 pt-4 border-t border-gray-200">
<h4 className="text-sm font-semibold text-gray-700 mb-3">Zustaendige Aufsichtsbehoerden</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{supervisoryAuthorities.map((sa, idx) => (
<div key={idx} className="flex items-start gap-3 bg-gray-50 rounded-lg p-3">
<div className="flex-shrink-0 w-6 h-6 bg-blue-100 rounded flex items-center justify-center mt-0.5">
<svg className="w-4 h-4 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
</div>
<div>
<span className="text-sm font-medium text-gray-900">{sa.authority.abbreviation}</span>
<span className="text-xs text-gray-500 ml-1">({sa.domain})</span>
<p className="text-xs text-gray-600 mt-0.5">{sa.authority.name}</p>
{sa.authority.url && (
<a
href={sa.authority.url}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-purple-600 hover:text-purple-700"
>
Website
</a>
)}
</div>
</div>
))}
</div>
</div>
)}
{/* Link to Obligations */}
{onGoToObligations && (
<div className="mt-4 pt-4 border-t border-gray-200">
<button
onClick={onGoToObligations}
className="inline-flex items-center gap-2 px-4 py-2 text-sm bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
>
Pflichten anzeigen
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7l5 5m0 0l-5 5m5-5H6" />
</svg>
</button>
</div>
)}
</div>
) : (
<p className="text-gray-500 text-sm">Keine anwendbaren Regulierungen ermittelt.</p>
)}
</div>
)}
{/* Hard Triggers */}
{decision.triggeredHardTriggers && decision.triggeredHardTriggers.length > 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Hard-Trigger</h3>
<div className="space-y-3">
{decision.triggeredHardTriggers.map((trigger, idx) => (
<div
key={idx}
className="border rounded-lg overflow-hidden border-red-300 bg-red-50"
>
<button
type="button"
onClick={() => setExpandedTrigger(expandedTrigger === idx ? null : idx)}
className="w-full px-4 py-3 flex items-center justify-between hover:bg-opacity-80 transition-colors"
>
<div className="flex items-center gap-3">
<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>
<span className="font-medium text-gray-900">{trigger.description}</span>
<span className="text-xs px-2 py-0.5 rounded-full bg-red-200 text-red-800 font-medium">
Min. {trigger.minimumLevel}
</span>
</div>
<svg
className={`w-5 h-5 text-gray-500 transition-transform ${
expandedTrigger === idx ? 'rotate-180' : ''
}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{expandedTrigger === idx && (
<div className="px-4 pb-4 pt-2 border-t border-gray-200">
<p className="text-sm text-gray-700 mb-2">{trigger.description}</p>
{trigger.legalReference && (
<p className="text-xs text-gray-600 mb-2">
<span className="font-medium">Rechtsgrundlage:</span> {trigger.legalReference}
</p>
)}
{trigger.category && (
<p className="text-xs text-gray-700">
<span className="font-medium">Kategorie:</span> {trigger.category}
</p>
)}
{trigger.requiresDSFA && (
<p className="text-xs text-orange-700 font-medium mt-1">DSFA erforderlich</p>
)}
</div>
)}
</div>
))}
</div>
</div>
)}
{/* Required Documents */}
{decision.requiredDocuments && decision.requiredDocuments.length > 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Erforderliche Dokumente</h3>
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-gray-200">
<th className="text-left py-3 px-4 text-sm font-semibold text-gray-700">Typ</th>
<th className="text-left py-3 px-4 text-sm font-semibold text-gray-700">Tiefe</th>
<th className="text-left py-3 px-4 text-sm font-semibold text-gray-700">Aufwand</th>
<th className="text-left py-3 px-4 text-sm font-semibold text-gray-700">Status</th>
<th className="text-left py-3 px-4 text-sm font-semibold text-gray-700">Aktion</th>
</tr>
</thead>
<tbody>
{decision.requiredDocuments.map((doc, idx) => (
<tr key={idx} className="border-b border-gray-100 last:border-b-0 hover:bg-gray-50">
<td className="py-3 px-4">
<div className="flex items-center gap-2">
<span className="font-medium text-gray-900">
{DOCUMENT_TYPE_LABELS[doc.documentType] || doc.documentType}
</span>
{doc.isMandatory && (
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-purple-100 text-purple-800">
Pflicht
</span>
)}
</div>
</td>
<td className="py-3 px-4 text-sm text-gray-700">{doc.depthDescription}</td>
<td className="py-3 px-4 text-sm text-gray-700">
{doc.effortEstimate ? `${doc.effortEstimate.days} Tage` : '-'}
</td>
<td className="py-3 px-4">
{doc.triggeredByHardTrigger && (
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-100 text-red-800">
Hard-Trigger
</span>
)}
</td>
<td className="py-3 px-4">
{doc.sdkStepUrl && (
<a
href={doc.sdkStepUrl}
className="text-sm text-purple-600 hover:text-purple-700 font-medium"
>
Zum SDK-Schritt
</a>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{/* Risk Flags */}
{decision.riskFlags && decision.riskFlags.length > 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Risiko-Flags</h3>
<div className="space-y-4">
{decision.riskFlags.map((flag, idx) => (
<div key={idx} className="border border-gray-200 rounded-lg p-4">
<div className="flex items-start justify-between mb-2">
<h4 className="font-semibold text-gray-900">{flag.title}</h4>
{getSeverityBadge(flag.severity)}
</div>
<p className="text-sm text-gray-700 mb-2">{flag.description}</p>
<p className="text-sm text-gray-600">
<span className="font-medium">Empfehlung:</span> {flag.recommendation}
</p>
</div>
))}
</div>
</div>
)}
{/* Gap Analysis */}
{decision.gapAnalysis && decision.gapAnalysis.length > 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Gap-Analyse</h3>
<div className="space-y-4">
{decision.gapAnalysis.map((gap, idx) => (
<div key={idx} className="border border-gray-200 rounded-lg p-4">
<div className="flex items-start justify-between mb-2">
<h4 className="font-semibold text-gray-900">{gap.title}</h4>
{getSeverityBadge(gap.severity)}
</div>
<p className="text-sm text-gray-700 mb-2">{gap.description}</p>
<p className="text-sm text-gray-600 mb-2">
<span className="font-medium">Empfehlung:</span> {gap.recommendation}
</p>
{gap.relatedDocuments && gap.relatedDocuments.length > 0 && (
<div className="mt-2">
<span className="text-xs text-gray-500">Betroffene Dokumente: </span>
{gap.relatedDocuments.map((doc, docIdx) => (
<span
key={docIdx}
className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-800 mr-1"
>
{DOCUMENT_TYPE_LABELS[doc] || doc}
</span>
))}
</div>
)}
</div>
))}
</div>
</div>
)}
{/* Next Actions */}
{decision.nextActions && decision.nextActions.length > 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Nächste Schritte</h3>
<div className="space-y-4">
{decision.nextActions.map((action, idx) => (
<div key={idx} className="flex gap-4">
<div className="flex-shrink-0 w-8 h-8 bg-purple-100 rounded-full flex items-center justify-center">
<span className="text-sm font-bold text-purple-700">{action.priority}</span>
</div>
<div className="flex-1">
<h4 className="font-semibold text-gray-900 mb-1">{action.title}</h4>
<p className="text-sm text-gray-700 mb-2">{action.description}</p>
<div className="flex items-center gap-3">
{action.effortDays && (
<span className="text-xs text-gray-600">
<span className="font-medium">Aufwand:</span> {action.effortDays} Tage
</span>
)}
{action.relatedDocuments && action.relatedDocuments.length > 0 && (
<span className="text-xs text-gray-600">
<span className="font-medium">Dokumente:</span> {action.relatedDocuments.length}
</span>
)}
</div>
</div>
</div>
))}
</div>
</div>
)}
{/* Action Buttons */}
<div className="flex items-center gap-3">
{onBackToWizard && (
<button
onClick={onBackToWizard}
className="px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
>
Zurueck zum Wizard
</button>
)}
{canEvaluate && onEvaluate && (
<button
onClick={onEvaluate}
disabled={isEvaluating}
className="px-4 py-2 text-sm bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 transition-colors"
>
{isEvaluating ? 'Bewertung laeuft...' : 'Neu bewerten'}
</button>
)}
{onGoToExport && (
<button
onClick={onGoToExport}
className="px-4 py-2 text-sm bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
>
Zum Export
</button>
)}
</div>
{/* Audit Trail */}
{decision.auditTrail && decision.auditTrail.length > 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-6">
<button
type="button"
onClick={() => setShowAuditTrail(!showAuditTrail)}
className="w-full flex items-center justify-between mb-4"
>
<h3 className="text-lg font-semibold text-gray-900">Audit-Trail</h3>
<svg
className={`w-5 h-5 text-gray-500 transition-transform ${showAuditTrail ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{showAuditTrail && (
<div className="space-y-3">
{decision.auditTrail.map((entry, idx) => (
<div key={idx} className="border-l-2 border-purple-300 pl-4 py-2">
<h4 className="font-medium text-gray-900 mb-1">{entry.step}</h4>
<p className="text-sm text-gray-700 mb-2">{entry.description}</p>
{entry.details && entry.details.length > 0 && (
<ul className="text-xs text-gray-600 space-y-1">
{entry.details.map((detail, detailIdx) => (
<li key={detailIdx}> {detail}</li>
))}
</ul>
)}
</div>
))}
</div>
)}
</div>
)}
</div>
)
}