Some checks failed
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
ci/woodpecker/manual/build-ci-image Pipeline was successful
ci/woodpecker/manual/main Pipeline failed
All services: admin-v2, studio-v2, website, ai-compliance-sdk, consent-service, klausur-service, voice-service, and infrastructure. Large PDFs and compiled binaries excluded via .gitignore.
221 lines
8.9 KiB
TypeScript
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>
|
|
)
|
|
}
|