refactor: Admin-Layout komplett entfernt — SDK als einziges Layout
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
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>
This commit is contained in:
@@ -0,0 +1,296 @@
|
||||
'use client'
|
||||
|
||||
import { useMemo, useState } from 'react'
|
||||
import {
|
||||
DataPoint,
|
||||
DataPointCategory,
|
||||
CATEGORY_METADATA,
|
||||
RISK_LEVEL_STYLING,
|
||||
RiskLevel,
|
||||
} from '@/lib/sdk/einwilligungen/types'
|
||||
|
||||
interface DataPointsPreviewProps {
|
||||
dataPoints: DataPoint[]
|
||||
onInsertPlaceholder: (placeholder: string) => void
|
||||
language?: 'de' | 'en'
|
||||
}
|
||||
|
||||
/**
|
||||
* Platzhalter-Definitionen mit Beschreibungen
|
||||
*/
|
||||
const PLACEHOLDERS = [
|
||||
{
|
||||
placeholder: '[DATENPUNKTE_TABLE]',
|
||||
label: { de: 'Tabelle', en: 'Table' },
|
||||
description: { de: 'Markdown-Tabelle mit allen Datenpunkten', en: 'Markdown table with all data points' },
|
||||
},
|
||||
{
|
||||
placeholder: '[DATENPUNKTE_LIST]',
|
||||
label: { de: 'Liste', en: 'List' },
|
||||
description: { de: 'Kommaseparierte Liste der Namen', en: 'Comma-separated list of names' },
|
||||
},
|
||||
{
|
||||
placeholder: '[VERARBEITUNGSZWECKE]',
|
||||
label: { de: 'Zwecke', en: 'Purposes' },
|
||||
description: { de: 'Alle Verarbeitungszwecke', en: 'All processing purposes' },
|
||||
},
|
||||
{
|
||||
placeholder: '[RECHTSGRUNDLAGEN]',
|
||||
label: { de: 'Rechtsgrundlagen', en: 'Legal Bases' },
|
||||
description: { de: 'DSGVO-Artikel', en: 'GDPR articles' },
|
||||
},
|
||||
{
|
||||
placeholder: '[SPEICHERFRISTEN]',
|
||||
label: { de: 'Speicherfristen', en: 'Retention' },
|
||||
description: { de: 'Fristen nach Kategorie', en: 'Periods by category' },
|
||||
},
|
||||
{
|
||||
placeholder: '[EMPFAENGER]',
|
||||
label: { de: 'Empfänger', en: 'Recipients' },
|
||||
description: { de: 'Liste aller Drittparteien', en: 'List of third parties' },
|
||||
},
|
||||
{
|
||||
placeholder: '[BESONDERE_KATEGORIEN]',
|
||||
label: { de: 'Art. 9', en: 'Art. 9' },
|
||||
description: { de: 'Abschnitt für sensible Daten', en: 'Section for sensitive data' },
|
||||
},
|
||||
{
|
||||
placeholder: '[DRITTLAND_TRANSFERS]',
|
||||
label: { de: 'Drittländer', en: 'Third Countries' },
|
||||
description: { de: 'Datenübermittlung außerhalb EU', en: 'Data transfers outside EU' },
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* Risiko-Badge Farben
|
||||
*/
|
||||
function getRiskBadgeColor(riskLevel: RiskLevel): string {
|
||||
switch (riskLevel) {
|
||||
case 'HIGH':
|
||||
return 'bg-red-100 text-red-700 border-red-200'
|
||||
case 'MEDIUM':
|
||||
return 'bg-yellow-100 text-yellow-700 border-yellow-200'
|
||||
case 'LOW':
|
||||
default:
|
||||
return 'bg-green-100 text-green-700 border-green-200'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DataPointsPreview Komponente
|
||||
*/
|
||||
export function DataPointsPreview({
|
||||
dataPoints,
|
||||
onInsertPlaceholder,
|
||||
language = 'de',
|
||||
}: DataPointsPreviewProps) {
|
||||
const [expandedCategories, setExpandedCategories] = useState<string[]>([])
|
||||
|
||||
// Gruppiere Datenpunkte nach Kategorie
|
||||
const byCategory = useMemo(() => {
|
||||
return dataPoints.reduce((acc, dp) => {
|
||||
if (!acc[dp.category]) {
|
||||
acc[dp.category] = []
|
||||
}
|
||||
acc[dp.category].push(dp)
|
||||
return acc
|
||||
}, {} as Record<DataPointCategory, DataPoint[]>)
|
||||
}, [dataPoints])
|
||||
|
||||
// Statistiken berechnen
|
||||
const stats = useMemo(() => {
|
||||
const riskCounts: Record<RiskLevel, number> = { LOW: 0, MEDIUM: 0, HIGH: 0 }
|
||||
let specialCategoryCount = 0
|
||||
|
||||
dataPoints.forEach(dp => {
|
||||
riskCounts[dp.riskLevel]++
|
||||
if (dp.isSpecialCategory) specialCategoryCount++
|
||||
})
|
||||
|
||||
return {
|
||||
riskCounts,
|
||||
specialCategoryCount,
|
||||
categoryCount: Object.keys(byCategory).length,
|
||||
}
|
||||
}, [dataPoints, byCategory])
|
||||
|
||||
// Sortierte Kategorien (nach Code)
|
||||
const sortedCategories = useMemo(() => {
|
||||
return Object.entries(byCategory).sort((a, b) => {
|
||||
const codeA = CATEGORY_METADATA[a[0] as DataPointCategory]?.code || 'Z'
|
||||
const codeB = CATEGORY_METADATA[b[0] as DataPointCategory]?.code || 'Z'
|
||||
return codeA.localeCompare(codeB)
|
||||
})
|
||||
}, [byCategory])
|
||||
|
||||
const toggleCategory = (category: string) => {
|
||||
setExpandedCategories(prev =>
|
||||
prev.includes(category)
|
||||
? prev.filter(c => c !== category)
|
||||
: [...prev, category]
|
||||
)
|
||||
}
|
||||
|
||||
if (dataPoints.length === 0) {
|
||||
return (
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<h4 className="font-semibold text-gray-900 flex items-center gap-2 mb-3">
|
||||
<svg className="w-5 h-5 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4" />
|
||||
</svg>
|
||||
{language === 'de' ? 'Einwilligungen' : 'Consents'}
|
||||
</h4>
|
||||
<p className="text-sm text-gray-500">
|
||||
{language === 'de'
|
||||
? 'Keine Datenpunkte ausgewählt. Wählen Sie Datenpunkte im Einwilligungs-Schritt aus.'
|
||||
: 'No data points selected. Select data points in the consent step.'}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6 h-full flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="mb-4">
|
||||
<h4 className="font-semibold text-gray-900 flex items-center gap-2">
|
||||
<svg className="w-5 h-5 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4" />
|
||||
</svg>
|
||||
{language === 'de' ? 'Einwilligungen' : 'Consents'}
|
||||
</h4>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
{dataPoints.length} {language === 'de' ? 'Datenpunkte aus' : 'data points from'}{' '}
|
||||
{stats.categoryCount} {language === 'de' ? 'Kategorien' : 'categories'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Statistik-Badges */}
|
||||
<div className="flex flex-wrap gap-2 mb-4">
|
||||
{stats.riskCounts.HIGH > 0 && (
|
||||
<span className="px-2 py-1 text-xs rounded-full bg-red-100 text-red-700">
|
||||
{stats.riskCounts.HIGH} {language === 'de' ? 'Hoch' : 'High'}
|
||||
</span>
|
||||
)}
|
||||
{stats.riskCounts.MEDIUM > 0 && (
|
||||
<span className="px-2 py-1 text-xs rounded-full bg-yellow-100 text-yellow-700">
|
||||
{stats.riskCounts.MEDIUM} {language === 'de' ? 'Mittel' : 'Medium'}
|
||||
</span>
|
||||
)}
|
||||
{stats.riskCounts.LOW > 0 && (
|
||||
<span className="px-2 py-1 text-xs rounded-full bg-green-100 text-green-700">
|
||||
{stats.riskCounts.LOW} {language === 'de' ? 'Niedrig' : 'Low'}
|
||||
</span>
|
||||
)}
|
||||
{stats.specialCategoryCount > 0 && (
|
||||
<span className="px-2 py-1 text-xs rounded-full bg-orange-100 text-orange-700 flex items-center gap-1">
|
||||
<svg className="w-3 h-3" 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>
|
||||
{stats.specialCategoryCount} Art. 9
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-200 my-3"></div>
|
||||
|
||||
{/* Datenpunkte nach Kategorie */}
|
||||
<div className="flex-1 overflow-y-auto space-y-2 max-h-64">
|
||||
{sortedCategories.map(([category, points]) => {
|
||||
const metadata = CATEGORY_METADATA[category as DataPointCategory]
|
||||
if (!metadata) return null
|
||||
const isExpanded = expandedCategories.includes(category)
|
||||
|
||||
return (
|
||||
<div key={category} className="border border-gray-100 rounded-lg">
|
||||
<button
|
||||
onClick={() => toggleCategory(category)}
|
||||
className="w-full flex items-center justify-between p-2 text-sm hover:bg-gray-50 rounded-lg"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono text-xs text-gray-400">{metadata.code}</span>
|
||||
<span className="font-medium text-gray-900">
|
||||
{language === 'de' ? metadata.name.de : metadata.name.en}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="px-1.5 py-0.5 text-xs rounded bg-gray-100 text-gray-600">
|
||||
{points.length}
|
||||
</span>
|
||||
<svg
|
||||
className={`w-4 h-4 text-gray-400 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>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{isExpanded && (
|
||||
<ul className="px-2 pb-2 space-y-1">
|
||||
{points.map(dp => (
|
||||
<li
|
||||
key={dp.id}
|
||||
className="flex items-center justify-between text-sm py-1 pl-6"
|
||||
>
|
||||
<span className="truncate max-w-[160px] text-gray-700">
|
||||
{language === 'de' ? dp.name.de : dp.name.en}
|
||||
</span>
|
||||
<div className="flex items-center gap-1">
|
||||
{dp.isSpecialCategory && (
|
||||
<svg className="w-3 h-3 text-orange-500" 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={`px-1.5 py-0.5 text-[10px] rounded border ${getRiskBadgeColor(dp.riskLevel)}`}>
|
||||
{RISK_LEVEL_STYLING[dp.riskLevel].label[language]}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-200 my-3"></div>
|
||||
|
||||
{/* Schnell-Einfügen Buttons */}
|
||||
<div>
|
||||
<p className="text-xs font-medium text-gray-500 mb-2">
|
||||
{language === 'de' ? 'Platzhalter einfügen:' : 'Insert placeholder:'}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{PLACEHOLDERS.slice(0, 4).map(({ placeholder, label }) => (
|
||||
<button
|
||||
key={placeholder}
|
||||
onClick={() => onInsertPlaceholder(placeholder)}
|
||||
className="px-2 py-1 text-xs bg-purple-50 text-purple-700 rounded hover:bg-purple-100 transition-colors"
|
||||
title={placeholder}
|
||||
>
|
||||
{language === 'de' ? label.de : label.en}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1.5 mt-1.5">
|
||||
{PLACEHOLDERS.slice(4).map(({ placeholder, label }) => (
|
||||
<button
|
||||
key={placeholder}
|
||||
onClick={() => onInsertPlaceholder(placeholder)}
|
||||
className="px-2 py-1 text-xs bg-purple-50 text-purple-700 rounded hover:bg-purple-100 transition-colors"
|
||||
title={placeholder}
|
||||
>
|
||||
{language === 'de' ? label.de : label.en}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DataPointsPreview
|
||||
@@ -0,0 +1,211 @@
|
||||
'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
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Document Generator Components
|
||||
*
|
||||
* Diese Komponenten integrieren die Einwilligungen-Datenpunkte
|
||||
* in den Dokumentengenerator.
|
||||
*/
|
||||
|
||||
export { DataPointsPreview } from './DataPointsPreview'
|
||||
export { DocumentValidation } from './DocumentValidation'
|
||||
Reference in New Issue
Block a user