All checks were successful
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) Successful in 32s
CI / test-python-backend-compliance (push) Successful in 31s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 19s
Kaputtes (admin) Layout geloescht (Role-Selection, 404-Sidebar, localhost-Dashboard). SDK-Flow nach /sdk/sdk-flow verschoben. Route-Gruppe (sdk) aufgeloest. Root-Seite redirected auf /sdk. ~25 ungenutzte Dateien/Verzeichnisse entfernt. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
212 lines
8.6 KiB
TypeScript
212 lines
8.6 KiB
TypeScript
'use client'
|
|
|
|
import { useMemo, useState } from 'react'
|
|
import { DataPoint } from '@/lib/sdk/einwilligungen/types'
|
|
import {
|
|
validateDocument,
|
|
ValidationWarning,
|
|
} from '@/lib/sdk/document-generator/datapoint-helpers'
|
|
|
|
interface DocumentValidationProps {
|
|
dataPoints: DataPoint[]
|
|
documentContent: string
|
|
language?: 'de' | 'en'
|
|
onInsertPlaceholder?: (placeholder: string) => void
|
|
}
|
|
|
|
/**
|
|
* Placeholder-Vorschlag aus der Warnung extrahieren
|
|
*/
|
|
function extractPlaceholderSuggestion(warning: ValidationWarning): string | null {
|
|
const match = warning.suggestion.match(/\[([A-Z_]+)\]/)
|
|
return match ? match[0] : null
|
|
}
|
|
|
|
/**
|
|
* DocumentValidation Komponente
|
|
*/
|
|
export function DocumentValidation({
|
|
dataPoints,
|
|
documentContent,
|
|
language = 'de',
|
|
onInsertPlaceholder,
|
|
}: DocumentValidationProps) {
|
|
const [expandedWarnings, setExpandedWarnings] = useState<string[]>([])
|
|
|
|
// Führe Validierung durch
|
|
const warnings = useMemo(() => {
|
|
if (dataPoints.length === 0 || !documentContent) {
|
|
return []
|
|
}
|
|
return validateDocument(dataPoints, documentContent, language)
|
|
}, [dataPoints, documentContent, language])
|
|
|
|
// Gruppiere nach Typ
|
|
const errorCount = warnings.filter(w => w.type === 'error').length
|
|
const warningCount = warnings.filter(w => w.type === 'warning').length
|
|
const infoCount = warnings.filter(w => w.type === 'info').length
|
|
|
|
const toggleWarning = (code: string) => {
|
|
setExpandedWarnings(prev =>
|
|
prev.includes(code) ? prev.filter(c => c !== code) : [...prev, code]
|
|
)
|
|
}
|
|
|
|
if (warnings.length === 0) {
|
|
// Keine Warnungen - zeige Erfolgsmeldung wenn Datenpunkte vorhanden
|
|
if (dataPoints.length > 0 && documentContent.length > 100) {
|
|
return (
|
|
<div className="bg-green-50 border border-green-200 rounded-xl p-4">
|
|
<div className="flex items-start gap-3">
|
|
<svg className="w-5 h-5 text-green-600 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
<div>
|
|
<h4 className="font-medium text-green-800">
|
|
{language === 'de' ? 'Dokument valide' : 'Document valid'}
|
|
</h4>
|
|
<p className="text-sm text-green-700 mt-1">
|
|
{language === 'de'
|
|
? 'Alle notwendigen Abschnitte für die ausgewählten Datenpunkte sind vorhanden.'
|
|
: 'All necessary sections for the selected data points are present.'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-3">
|
|
{/* Zusammenfassung */}
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<span className="font-medium text-gray-700">
|
|
{language === 'de' ? 'Validierung:' : 'Validation:'}
|
|
</span>
|
|
{errorCount > 0 && (
|
|
<span className="px-2 py-0.5 text-xs rounded-full bg-red-100 text-red-700">
|
|
{errorCount} {language === 'de' ? 'Fehler' : 'Error'}{errorCount > 1 && 's'}
|
|
</span>
|
|
)}
|
|
{warningCount > 0 && (
|
|
<span className="px-2 py-0.5 text-xs rounded-full bg-yellow-100 text-yellow-700">
|
|
{warningCount} {language === 'de' ? 'Warnung' : 'Warning'}{warningCount > 1 && (language === 'de' ? 'en' : 's')}
|
|
</span>
|
|
)}
|
|
{infoCount > 0 && (
|
|
<span className="px-2 py-0.5 text-xs rounded-full bg-blue-100 text-blue-700">
|
|
{infoCount} {language === 'de' ? 'Hinweis' : 'Info'}{infoCount > 1 && (language === 'de' ? 'e' : 's')}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Warnungen */}
|
|
{warnings.map((warning, index) => {
|
|
const placeholder = extractPlaceholderSuggestion(warning)
|
|
const isExpanded = expandedWarnings.includes(warning.code)
|
|
const isError = warning.type === 'error'
|
|
|
|
return (
|
|
<div
|
|
key={`${warning.code}-${index}`}
|
|
className={`rounded-xl border p-4 ${
|
|
isError
|
|
? 'bg-red-50 border-red-200'
|
|
: 'bg-yellow-50 border-yellow-200'
|
|
}`}
|
|
>
|
|
<div className="flex items-start gap-3">
|
|
{/* Icon */}
|
|
<svg
|
|
className={`w-5 h-5 mt-0.5 ${isError ? 'text-red-600' : 'text-yellow-600'}`}
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
{isError ? (
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
) : (
|
|
<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>
|
|
|
|
<div className="flex-1">
|
|
{/* Message */}
|
|
<p className={`font-medium ${isError ? 'text-red-800' : 'text-yellow-800'}`}>
|
|
{warning.message}
|
|
</p>
|
|
|
|
{/* Suggestion */}
|
|
<div className="flex items-start gap-2 mt-2">
|
|
<svg className="w-4 h-4 mt-0.5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
|
</svg>
|
|
<span className="text-sm text-gray-600">{warning.suggestion}</span>
|
|
</div>
|
|
|
|
{/* Quick-Fix Button */}
|
|
{placeholder && onInsertPlaceholder && (
|
|
<button
|
|
onClick={() => onInsertPlaceholder(placeholder)}
|
|
className="mt-3 inline-flex items-center gap-1.5 px-3 py-1.5 text-sm bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
|
</svg>
|
|
{language === 'de' ? 'Platzhalter einfügen' : 'Insert placeholder'}
|
|
<code className="ml-1 text-xs bg-gray-100 px-1.5 py-0.5 rounded">
|
|
{placeholder}
|
|
</code>
|
|
</button>
|
|
)}
|
|
|
|
{/* Betroffene Datenpunkte */}
|
|
{warning.affectedDataPoints && warning.affectedDataPoints.length > 0 && (
|
|
<div className="mt-3">
|
|
<button
|
|
onClick={() => toggleWarning(warning.code)}
|
|
className="flex items-center gap-1 text-xs text-gray-500 hover:text-gray-700"
|
|
>
|
|
<svg
|
|
className={`w-3 h-3 transition-transform ${isExpanded ? '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>
|
|
{warning.affectedDataPoints.length}{' '}
|
|
{language === 'de' ? 'betroffene Datenpunkte' : 'affected data points'}
|
|
</button>
|
|
|
|
{isExpanded && (
|
|
<ul className="mt-2 text-xs space-y-0.5 pl-4">
|
|
{warning.affectedDataPoints.slice(0, 5).map(dp => (
|
|
<li key={dp.id} className="list-disc text-gray-600">
|
|
{language === 'de' ? dp.name.de : dp.name.en}
|
|
</li>
|
|
))}
|
|
{warning.affectedDataPoints.length > 5 && (
|
|
<li className="list-none text-gray-400">
|
|
... {language === 'de' ? 'und' : 'and'}{' '}
|
|
{warning.affectedDataPoints.length - 5}{' '}
|
|
{language === 'de' ? 'weitere' : 'more'}
|
|
</li>
|
|
)}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default DocumentValidation
|