SDK modules added/enhanced: - compliance-hub, compliance-scope, consent-management, notfallplan - audit-report, workflow, source-policy, dsms - advisory-board documentation section - TOM dashboard components, TOM generator SDM mapping - DSFA: mitigation library, risk catalog, threshold analysis, source attribution - VVT: baseline catalog, profiling engine, types - Loeschfristen: baseline catalog, compliance engine, export, profiling, types - Compliance scope: engine, profiling, golden tests, types Existing SDK pages updated: - dsfa/[id], tom, vvt, loeschfristen, advisory-board — expanded functionality - SDKSidebar, StepHeader — new navigation items and layout - SDK layout, context, types — expanded type system Other admin-v2 changes: - AI agents page, RAG pipeline DSFA integration - GridOverlay component updates - Companion feature (development + education) - Compliance advisor SOUL definition Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
335 lines
14 KiB
TypeScript
335 lines
14 KiB
TypeScript
'use client'
|
|
import React, { useState, useCallback } from 'react'
|
|
import type { ScopeDecision, ScopeProfilingAnswer } from '@/lib/sdk/compliance-scope-types'
|
|
import { DEPTH_LEVEL_LABELS, DOCUMENT_TYPE_LABELS } from '@/lib/sdk/compliance-scope-types'
|
|
|
|
interface ScopeExportTabProps {
|
|
decision: ScopeDecision | null
|
|
answers: ScopeProfilingAnswer[]
|
|
}
|
|
|
|
export function ScopeExportTab({ decision, answers }: ScopeExportTabProps) {
|
|
const [copiedMarkdown, setCopiedMarkdown] = useState(false)
|
|
|
|
const handleDownloadJSON = useCallback(() => {
|
|
if (!decision) return
|
|
const dataStr = JSON.stringify(decision, null, 2)
|
|
const dataBlob = new Blob([dataStr], { type: 'application/json' })
|
|
const url = URL.createObjectURL(dataBlob)
|
|
const link = document.createElement('a')
|
|
link.href = url
|
|
link.download = `compliance-scope-decision-${new Date().toISOString().split('T')[0]}.json`
|
|
link.click()
|
|
URL.revokeObjectURL(url)
|
|
}, [decision])
|
|
|
|
const handleDownloadCSV = useCallback(() => {
|
|
if (!decision || !decision.requiredDocuments) return
|
|
|
|
const headers = ['Typ', 'Tiefe', 'Aufwand (Tage)', 'Pflicht', 'Hard-Trigger']
|
|
const rows = decision.requiredDocuments.map((doc) => [
|
|
DOCUMENT_TYPE_LABELS[doc.documentType] || doc.documentType,
|
|
doc.depthDescription,
|
|
doc.effortEstimate?.days?.toString() || '0',
|
|
doc.isMandatory ? 'Ja' : 'Nein',
|
|
doc.triggeredByHardTrigger ? 'Ja' : 'Nein',
|
|
])
|
|
|
|
const csvContent = [headers, ...rows].map((row) => row.map((cell) => `"${cell}"`).join(',')).join('\n')
|
|
|
|
const dataBlob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' })
|
|
const url = URL.createObjectURL(dataBlob)
|
|
const link = document.createElement('a')
|
|
link.href = url
|
|
link.download = `compliance-scope-documents-${new Date().toISOString().split('T')[0]}.csv`
|
|
link.click()
|
|
URL.revokeObjectURL(url)
|
|
}, [decision])
|
|
|
|
const generateMarkdownSummary = useCallback(() => {
|
|
if (!decision) return ''
|
|
|
|
let markdown = `# Compliance Scope Entscheidung\n\n`
|
|
markdown += `**Datum:** ${new Date().toLocaleDateString('de-DE')}\n\n`
|
|
markdown += `## Einstufung\n\n`
|
|
markdown += `**Level:** ${decision.level} - ${DEPTH_LEVEL_LABELS[decision.level]}\n\n`
|
|
if (decision.reasoning) {
|
|
markdown += `**Begründung:** ${decision.reasoning}\n\n`
|
|
}
|
|
|
|
if (decision.scores) {
|
|
markdown += `## Scores\n\n`
|
|
markdown += `- **Risiko-Score:** ${decision.scores.riskScore}/100\n`
|
|
markdown += `- **Komplexitäts-Score:** ${decision.scores.complexityScore}/100\n`
|
|
markdown += `- **Assurance-Score:** ${decision.scores.assuranceScore}/100\n`
|
|
markdown += `- **Gesamt-Score:** ${decision.scores.compositeScore}/100\n\n`
|
|
}
|
|
|
|
if (decision.hardTriggers && decision.hardTriggers.length > 0) {
|
|
const matchedTriggers = decision.hardTriggers.filter((ht) => ht.matched)
|
|
if (matchedTriggers.length > 0) {
|
|
markdown += `## Aktive Hard-Trigger\n\n`
|
|
matchedTriggers.forEach((trigger) => {
|
|
markdown += `- **${trigger.label}**\n`
|
|
markdown += ` - ${trigger.description}\n`
|
|
if (trigger.legalReference) {
|
|
markdown += ` - Rechtsgrundlage: ${trigger.legalReference}\n`
|
|
}
|
|
})
|
|
markdown += `\n`
|
|
}
|
|
}
|
|
|
|
if (decision.requiredDocuments && decision.requiredDocuments.length > 0) {
|
|
markdown += `## Erforderliche Dokumente\n\n`
|
|
markdown += `| Typ | Tiefe | Aufwand | Pflicht | Hard-Trigger |\n`
|
|
markdown += `|-----|-------|---------|---------|-------------|\n`
|
|
decision.requiredDocuments.forEach((doc) => {
|
|
markdown += `| ${DOCUMENT_TYPE_LABELS[doc.documentType] || doc.documentType} | ${doc.depthDescription} | ${
|
|
doc.effortEstimate?.days || 0
|
|
} Tage | ${doc.isMandatory ? 'Ja' : 'Nein'} | ${doc.triggeredByHardTrigger ? 'Ja' : 'Nein'} |\n`
|
|
})
|
|
markdown += `\n`
|
|
}
|
|
|
|
if (decision.riskFlags && decision.riskFlags.length > 0) {
|
|
markdown += `## Risiko-Flags\n\n`
|
|
decision.riskFlags.forEach((flag) => {
|
|
markdown += `### ${flag.title} (${flag.severity})\n\n`
|
|
markdown += `${flag.description}\n\n`
|
|
markdown += `**Empfehlung:** ${flag.recommendation}\n\n`
|
|
})
|
|
}
|
|
|
|
if (decision.nextActions && decision.nextActions.length > 0) {
|
|
markdown += `## Nächste Schritte\n\n`
|
|
decision.nextActions.forEach((action) => {
|
|
markdown += `${action.priority}. **${action.title}**\n`
|
|
markdown += ` ${action.description}\n`
|
|
if (action.effortDays) {
|
|
markdown += ` Aufwand: ${action.effortDays} Tage\n`
|
|
}
|
|
markdown += `\n`
|
|
})
|
|
}
|
|
|
|
return markdown
|
|
}, [decision])
|
|
|
|
const handleCopyMarkdown = useCallback(() => {
|
|
const markdown = generateMarkdownSummary()
|
|
navigator.clipboard.writeText(markdown).then(() => {
|
|
setCopiedMarkdown(true)
|
|
setTimeout(() => setCopiedMarkdown(false), 2000)
|
|
})
|
|
}, [generateMarkdownSummary])
|
|
|
|
const handlePrintView = useCallback(() => {
|
|
if (!decision) return
|
|
|
|
const markdown = generateMarkdownSummary()
|
|
const htmlContent = `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>Compliance Scope Entscheidung</title>
|
|
<style>
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
max-width: 900px;
|
|
margin: 40px auto;
|
|
padding: 20px;
|
|
line-height: 1.6;
|
|
}
|
|
h1 { color: #7c3aed; border-bottom: 3px solid #7c3aed; padding-bottom: 10px; }
|
|
h2 { color: #5b21b6; margin-top: 30px; border-bottom: 2px solid #e5e7eb; padding-bottom: 8px; }
|
|
h3 { color: #4c1d95; margin-top: 20px; }
|
|
table { width: 100%; border-collapse: collapse; margin: 20px 0; }
|
|
th, td { border: 1px solid #d1d5db; padding: 12px; text-align: left; }
|
|
th { background-color: #f3f4f6; font-weight: 600; }
|
|
ul { list-style-type: disc; padding-left: 20px; }
|
|
li { margin: 8px 0; }
|
|
@media print {
|
|
body { margin: 20px; }
|
|
h1, h2, h3 { page-break-after: avoid; }
|
|
table { page-break-inside: avoid; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<pre style="white-space: pre-wrap; font-family: inherit;">${markdown}</pre>
|
|
</body>
|
|
</html>
|
|
`
|
|
const printWindow = window.open('', '_blank')
|
|
if (printWindow) {
|
|
printWindow.document.write(htmlContent)
|
|
printWindow.document.close()
|
|
printWindow.focus()
|
|
setTimeout(() => printWindow.print(), 250)
|
|
}
|
|
}, [decision, generateMarkdownSummary])
|
|
|
|
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="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 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 Daten zum Export</h3>
|
|
<p className="text-gray-600">Bitte führen Sie zuerst das Scope-Profiling durch.</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* JSON Export */}
|
|
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<svg className="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
|
|
/>
|
|
</svg>
|
|
<h3 className="text-lg font-semibold text-gray-900">JSON Export</h3>
|
|
</div>
|
|
<p className="text-sm text-gray-600 mb-3">
|
|
Exportieren Sie die vollständige Entscheidung als strukturierte JSON-Datei für weitere Verarbeitung oder
|
|
Archivierung.
|
|
</p>
|
|
<button
|
|
onClick={handleDownloadJSON}
|
|
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium text-sm"
|
|
>
|
|
JSON herunterladen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* CSV Export */}
|
|
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<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="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>
|
|
<h3 className="text-lg font-semibold text-gray-900">CSV Export</h3>
|
|
</div>
|
|
<p className="text-sm text-gray-600 mb-3">
|
|
Exportieren Sie die Liste der erforderlichen Dokumente als CSV-Datei für Excel, Google Sheets oder andere
|
|
Tabellenkalkulationen.
|
|
</p>
|
|
<button
|
|
onClick={handleDownloadCSV}
|
|
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors font-medium text-sm"
|
|
>
|
|
CSV herunterladen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Markdown Summary */}
|
|
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
|
<div className="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="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>
|
|
<h3 className="text-lg font-semibold text-gray-900">Markdown-Zusammenfassung</h3>
|
|
</div>
|
|
<p className="text-sm text-gray-600 mb-3">
|
|
Strukturierte Zusammenfassung im Markdown-Format für Dokumentation oder Berichte.
|
|
</p>
|
|
<textarea
|
|
readOnly
|
|
value={generateMarkdownSummary()}
|
|
className="w-full h-64 px-4 py-3 border border-gray-300 rounded-lg font-mono text-sm text-gray-700 resize-none focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
|
/>
|
|
<button
|
|
onClick={handleCopyMarkdown}
|
|
className="mt-3 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors font-medium text-sm"
|
|
>
|
|
{copiedMarkdown ? 'Kopiert!' : 'Kopieren'}
|
|
</button>
|
|
</div>
|
|
|
|
{/* Print View */}
|
|
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<svg className="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"
|
|
/>
|
|
</svg>
|
|
<h3 className="text-lg font-semibold text-gray-900">Druckansicht</h3>
|
|
</div>
|
|
<p className="text-sm text-gray-600 mb-3">
|
|
Öffnen Sie eine druckfreundliche HTML-Ansicht der Entscheidung in einem neuen Fenster.
|
|
</p>
|
|
<button
|
|
onClick={handlePrintView}
|
|
className="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors font-medium text-sm"
|
|
>
|
|
Druckansicht öffnen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Export Info */}
|
|
<div className="bg-blue-50 border border-blue-200 rounded-xl p-4">
|
|
<div className="flex gap-3">
|
|
<svg className="w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
/>
|
|
</svg>
|
|
<div>
|
|
<h4 className="text-sm font-semibold text-blue-900 mb-1">Export-Hinweise</h4>
|
|
<ul className="text-sm text-blue-800 space-y-1">
|
|
<li>• JSON-Exporte enthalten alle Daten und können wieder importiert werden</li>
|
|
<li>• CSV-Exporte sind ideal für Tabellenkalkulation und Aufwandsschätzungen</li>
|
|
<li>• Markdown eignet sich für Dokumentation und Berichte</li>
|
|
<li>• Die Druckansicht ist optimiert für PDF-Export über den Browser</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|