Files
breakpilot-compliance/admin-compliance/components/sdk/ValidationReport.tsx
Benjamin Boenisch 4435e7ea0a Initial commit: breakpilot-compliance - Compliance SDK Platform
Services: Admin-Compliance, Backend-Compliance,
AI-Compliance-SDK, Consent-SDK, Developer-Portal,
PCA-Platform, DSMS

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:28 +01:00

221 lines
8.9 KiB
TypeScript

'use client'
/**
* ValidationReport - Strukturierte Anzeige von Validierungsergebnissen
*
* Errors (Scope-Violations) in Rot
* Warnings (Inkonsistenzen) in Amber
* Suggestions in Blau
*/
import { DOCUMENT_TYPE_LABELS } from '@/lib/sdk/compliance-scope-types'
import type { ValidationResult, ValidationFinding } from '@/lib/sdk/drafting-engine/types'
interface ValidationReportProps {
result: ValidationResult
onClose: () => void
/** Compact mode for inline display in widget */
compact?: boolean
}
const SEVERITY_CONFIG = {
error: {
bg: 'bg-red-50',
border: 'border-red-200',
text: 'text-red-700',
icon: 'M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z',
label: 'Fehler',
dotColor: 'bg-red-500',
},
warning: {
bg: 'bg-amber-50',
border: 'border-amber-200',
text: 'text-amber-700',
icon: 'M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z',
label: 'Warnungen',
dotColor: 'bg-amber-500',
},
suggestion: {
bg: 'bg-blue-50',
border: 'border-blue-200',
text: 'text-blue-700',
icon: 'M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
label: 'Vorschlaege',
dotColor: 'bg-blue-500',
},
}
function FindingCard({ finding, compact }: { finding: ValidationFinding; compact?: boolean }) {
const config = SEVERITY_CONFIG[finding.severity]
const docLabel = DOCUMENT_TYPE_LABELS[finding.documentType]?.split(' (')[0] || finding.documentType
if (compact) {
return (
<div className={`flex items-start gap-2 px-2.5 py-1.5 ${config.bg} rounded-md border ${config.border}`}>
<span className={`w-1.5 h-1.5 rounded-full mt-1.5 shrink-0 ${config.dotColor}`} />
<div className="min-w-0">
<p className={`text-xs font-medium ${config.text}`}>{finding.title}</p>
<p className="text-xs text-gray-500 truncate">{finding.description}</p>
</div>
</div>
)
}
return (
<div className={`${config.bg} rounded-lg border ${config.border} p-3`}>
<div className="flex items-start gap-2">
<svg className={`w-4 h-4 mt-0.5 shrink-0 ${config.text}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={config.icon} />
</svg>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<h4 className={`text-sm font-medium ${config.text}`}>{finding.title}</h4>
<span className="text-xs text-gray-400 bg-gray-100 px-1.5 py-0.5 rounded">{docLabel}</span>
</div>
<p className="text-xs text-gray-600 mt-1">{finding.description}</p>
{finding.crossReferenceType && (
<p className="text-xs text-gray-500 mt-1">
Cross-Referenz: {DOCUMENT_TYPE_LABELS[finding.crossReferenceType]?.split(' (')[0] || finding.crossReferenceType}
</p>
)}
{finding.legalReference && (
<p className="text-xs text-gray-500 mt-1 font-mono">{finding.legalReference}</p>
)}
{finding.suggestion && (
<div className="mt-2 flex items-start gap-1.5 px-2.5 py-1.5 bg-white/60 rounded border border-gray-100">
<svg className="w-3.5 h-3.5 mt-0.5 text-gray-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
</svg>
<p className="text-xs text-gray-600">{finding.suggestion}</p>
</div>
)}
</div>
</div>
</div>
)
}
export function ValidationReport({ result, onClose, compact }: ValidationReportProps) {
const totalFindings = result.errors.length + result.warnings.length + result.suggestions.length
if (compact) {
return (
<div className="rounded-lg border border-gray-200 bg-white overflow-hidden">
<div className="flex items-center justify-between px-3 py-2 bg-gray-50 border-b border-gray-100">
<div className="flex items-center gap-2">
<span className={`w-2 h-2 rounded-full ${result.passed ? 'bg-green-500' : 'bg-red-500'}`} />
<span className="text-xs font-medium text-gray-700">
{result.passed ? 'Validierung bestanden' : 'Validierung fehlgeschlagen'}
</span>
<span className="text-xs text-gray-400">
({totalFindings} {totalFindings === 1 ? 'Fund' : 'Funde'})
</span>
</div>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="p-2 space-y-1.5 max-h-36 overflow-y-auto">
{result.errors.map((f) => <FindingCard key={f.id} finding={f} compact />)}
{result.warnings.map((f) => <FindingCard key={f.id} finding={f} compact />)}
{result.suggestions.map((f) => <FindingCard key={f.id} finding={f} compact />)}
</div>
</div>
)
}
return (
<div className="space-y-4">
{/* Summary Header */}
<div className={`rounded-lg border p-4 ${result.passed ? 'bg-green-50 border-green-200' : 'bg-red-50 border-red-200'}`}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${result.passed ? 'bg-green-100' : 'bg-red-100'}`}>
<svg className={`w-5 h-5 ${result.passed ? 'text-green-600' : 'text-red-600'}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={result.passed ? 'M5 13l4 4L19 7' : 'M6 18L18 6M6 6l12 12'} />
</svg>
</div>
<div>
<h3 className={`text-sm font-semibold ${result.passed ? 'text-green-800' : 'text-red-800'}`}>
{result.passed ? 'Validierung bestanden' : 'Validierung fehlgeschlagen'}
</h3>
<p className="text-xs text-gray-500">
Level {result.scopeLevel} | {new Date(result.timestamp).toLocaleString('de-DE')}
</p>
</div>
</div>
{/* Stats */}
<div className="flex items-center gap-3">
{result.errors.length > 0 && (
<div className="flex items-center gap-1">
<span className="w-2 h-2 rounded-full bg-red-500" />
<span className="text-xs font-medium text-red-700">{result.errors.length}</span>
</div>
)}
{result.warnings.length > 0 && (
<div className="flex items-center gap-1">
<span className="w-2 h-2 rounded-full bg-amber-500" />
<span className="text-xs font-medium text-amber-700">{result.warnings.length}</span>
</div>
)}
{result.suggestions.length > 0 && (
<div className="flex items-center gap-1">
<span className="w-2 h-2 rounded-full bg-blue-500" />
<span className="text-xs font-medium text-blue-700">{result.suggestions.length}</span>
</div>
)}
<button onClick={onClose} className="text-gray-400 hover:text-gray-600 ml-2">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
</div>
{/* Errors */}
{result.errors.length > 0 && (
<div>
<h4 className="text-xs font-semibold text-red-700 uppercase tracking-wide mb-2">
Fehler ({result.errors.length})
</h4>
<div className="space-y-2">
{result.errors.map((f) => <FindingCard key={f.id} finding={f} />)}
</div>
</div>
)}
{/* Warnings */}
{result.warnings.length > 0 && (
<div>
<h4 className="text-xs font-semibold text-amber-700 uppercase tracking-wide mb-2">
Warnungen ({result.warnings.length})
</h4>
<div className="space-y-2">
{result.warnings.map((f) => <FindingCard key={f.id} finding={f} />)}
</div>
</div>
)}
{/* Suggestions */}
{result.suggestions.length > 0 && (
<div>
<h4 className="text-xs font-semibold text-blue-700 uppercase tracking-wide mb-2">
Vorschlaege ({result.suggestions.length})
</h4>
<div className="space-y-2">
{result.suggestions.map((f) => <FindingCard key={f.id} finding={f} />)}
</div>
</div>
)}
</div>
)
}