feat(admin-v2): Major SDK/Compliance overhaul and new modules

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>
This commit is contained in:
BreakPilot Dev
2026-02-10 00:01:04 +01:00
parent 53219e3eaf
commit dff2ef796b
94 changed files with 29706 additions and 1039 deletions

View File

@@ -0,0 +1,362 @@
'use client'
import React, { useState } from 'react'
import type { ScopeDecision, ComplianceDepthLevel } from '@/lib/sdk/compliance-scope-types'
import { DEPTH_LEVEL_LABELS, DEPTH_LEVEL_DESCRIPTIONS, DEPTH_LEVEL_COLORS, DOCUMENT_TYPE_LABELS } from '@/lib/sdk/compliance-scope-types'
interface ScopeDecisionTabProps {
decision: ScopeDecision | null
}
export function ScopeDecisionTab({ decision }: ScopeDecisionTabProps) {
const [expandedTrigger, setExpandedTrigger] = useState<number | null>(null)
const [showAuditTrail, setShowAuditTrail] = useState(false)
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="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>
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-2">Keine Entscheidung vorhanden</h3>
<p className="text-gray-600">Bitte führen Sie zuerst das Scope-Profiling durch.</p>
</div>
)
}
const getScoreColor = (score: number): string => {
if (score >= 80) return 'from-red-500 to-red-600'
if (score >= 60) return 'from-orange-500 to-orange-600'
if (score >= 40) return 'from-yellow-500 to-yellow-600'
return 'from-green-500 to-green-600'
}
const getSeverityBadge = (severity: 'low' | 'medium' | 'high' | 'critical') => {
const colors = {
low: 'bg-gray-100 text-gray-800',
medium: 'bg-yellow-100 text-yellow-800',
high: 'bg-orange-100 text-orange-800',
critical: 'bg-red-100 text-red-800',
}
const labels = {
low: 'Niedrig',
medium: 'Mittel',
high: 'Hoch',
critical: 'Kritisch',
}
return (
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${colors[severity]}`}>
{labels[severity]}
</span>
)
}
const renderScoreBar = (label: string, score: number | undefined) => {
const value = score ?? 0
return (
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm font-medium text-gray-700">{label}</span>
<span className="text-sm font-bold text-gray-900">{value}/100</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-3 overflow-hidden">
<div
className={`h-full bg-gradient-to-r ${getScoreColor(value)} transition-all duration-500`}
style={{ width: `${value}%` }}
/>
</div>
</div>
)
}
return (
<div className="space-y-6">
{/* Level Determination */}
<div className={`${DEPTH_LEVEL_COLORS[decision.level].bg} border-2 ${DEPTH_LEVEL_COLORS[decision.level].border} rounded-xl p-6`}>
<div className="flex items-start gap-6">
<div className={`flex-shrink-0 w-20 h-20 ${DEPTH_LEVEL_COLORS[decision.level].badge} rounded-xl flex items-center justify-center`}>
<span className={`text-3xl font-bold ${DEPTH_LEVEL_COLORS[decision.level].text}`}>
{decision.level}
</span>
</div>
<div className="flex-1">
<h2 className={`text-2xl font-bold ${DEPTH_LEVEL_COLORS[decision.level].text} mb-2`}>
{DEPTH_LEVEL_LABELS[decision.level]}
</h2>
<p className="text-gray-700 mb-3">{DEPTH_LEVEL_DESCRIPTIONS[decision.level]}</p>
{decision.reasoning && (
<p className="text-sm text-gray-600 italic">{decision.reasoning}</p>
)}
</div>
</div>
</div>
{/* Score Breakdown */}
{decision.scores && (
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Score-Analyse</h3>
<div className="space-y-4">
{renderScoreBar('Risiko-Score', decision.scores.riskScore)}
{renderScoreBar('Komplexitäts-Score', decision.scores.complexityScore)}
{renderScoreBar('Assurance-Score', decision.scores.assuranceScore)}
<div className="pt-4 border-t border-gray-200">
{renderScoreBar('Gesamt-Score', decision.scores.compositeScore)}
</div>
</div>
</div>
)}
{/* Hard Triggers */}
{decision.hardTriggers && decision.hardTriggers.length > 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Hard-Trigger</h3>
<div className="space-y-3">
{decision.hardTriggers.map((trigger, idx) => (
<div
key={idx}
className={`border rounded-lg overflow-hidden ${
trigger.matched ? 'border-red-300 bg-red-50' : 'border-gray-200 bg-gray-50'
}`}
>
<button
type="button"
onClick={() => setExpandedTrigger(expandedTrigger === idx ? null : idx)}
className="w-full px-4 py-3 flex items-center justify-between hover:bg-opacity-80 transition-colors"
>
<div className="flex items-center gap-3">
{trigger.matched && (
<svg className="w-5 h-5 text-red-600" 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="font-medium text-gray-900">{trigger.label}</span>
</div>
<svg
className={`w-5 h-5 text-gray-500 transition-transform ${
expandedTrigger === idx ? '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>
</button>
{expandedTrigger === idx && (
<div className="px-4 pb-4 pt-2 border-t border-gray-200">
<p className="text-sm text-gray-700 mb-2">{trigger.description}</p>
{trigger.legalReference && (
<p className="text-xs text-gray-600 mb-2">
<span className="font-medium">Rechtsgrundlage:</span> {trigger.legalReference}
</p>
)}
{trigger.matchedValue && (
<p className="text-xs text-gray-700">
<span className="font-medium">Erfasster Wert:</span> {trigger.matchedValue}
</p>
)}
</div>
)}
</div>
))}
</div>
</div>
)}
{/* Required Documents */}
{decision.requiredDocuments && decision.requiredDocuments.length > 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Erforderliche Dokumente</h3>
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-gray-200">
<th className="text-left py-3 px-4 text-sm font-semibold text-gray-700">Typ</th>
<th className="text-left py-3 px-4 text-sm font-semibold text-gray-700">Tiefe</th>
<th className="text-left py-3 px-4 text-sm font-semibold text-gray-700">Aufwand</th>
<th className="text-left py-3 px-4 text-sm font-semibold text-gray-700">Status</th>
<th className="text-left py-3 px-4 text-sm font-semibold text-gray-700">Aktion</th>
</tr>
</thead>
<tbody>
{decision.requiredDocuments.map((doc, idx) => (
<tr key={idx} className="border-b border-gray-100 last:border-b-0 hover:bg-gray-50">
<td className="py-3 px-4">
<div className="flex items-center gap-2">
<span className="font-medium text-gray-900">
{DOCUMENT_TYPE_LABELS[doc.documentType] || doc.documentType}
</span>
{doc.isMandatory && (
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-purple-100 text-purple-800">
Pflicht
</span>
)}
</div>
</td>
<td className="py-3 px-4 text-sm text-gray-700">{doc.depthDescription}</td>
<td className="py-3 px-4 text-sm text-gray-700">
{doc.effortEstimate ? `${doc.effortEstimate.days} Tage` : '-'}
</td>
<td className="py-3 px-4">
{doc.triggeredByHardTrigger && (
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-100 text-red-800">
Hard-Trigger
</span>
)}
</td>
<td className="py-3 px-4">
{doc.sdkStepUrl && (
<a
href={doc.sdkStepUrl}
className="text-sm text-purple-600 hover:text-purple-700 font-medium"
>
Zum SDK-Schritt
</a>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{/* Risk Flags */}
{decision.riskFlags && decision.riskFlags.length > 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Risiko-Flags</h3>
<div className="space-y-4">
{decision.riskFlags.map((flag, idx) => (
<div key={idx} className="border border-gray-200 rounded-lg p-4">
<div className="flex items-start justify-between mb-2">
<h4 className="font-semibold text-gray-900">{flag.title}</h4>
{getSeverityBadge(flag.severity)}
</div>
<p className="text-sm text-gray-700 mb-2">{flag.description}</p>
<p className="text-sm text-gray-600">
<span className="font-medium">Empfehlung:</span> {flag.recommendation}
</p>
</div>
))}
</div>
</div>
)}
{/* Gap Analysis */}
{decision.gapAnalysis && decision.gapAnalysis.length > 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Gap-Analyse</h3>
<div className="space-y-4">
{decision.gapAnalysis.map((gap, idx) => (
<div key={idx} className="border border-gray-200 rounded-lg p-4">
<div className="flex items-start justify-between mb-2">
<h4 className="font-semibold text-gray-900">{gap.title}</h4>
{getSeverityBadge(gap.severity)}
</div>
<p className="text-sm text-gray-700 mb-2">{gap.description}</p>
<p className="text-sm text-gray-600 mb-2">
<span className="font-medium">Empfehlung:</span> {gap.recommendation}
</p>
{gap.relatedDocuments && gap.relatedDocuments.length > 0 && (
<div className="mt-2">
<span className="text-xs text-gray-500">Betroffene Dokumente: </span>
{gap.relatedDocuments.map((doc, docIdx) => (
<span
key={docIdx}
className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-800 mr-1"
>
{DOCUMENT_TYPE_LABELS[doc] || doc}
</span>
))}
</div>
)}
</div>
))}
</div>
</div>
)}
{/* Next Actions */}
{decision.nextActions && decision.nextActions.length > 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Nächste Schritte</h3>
<div className="space-y-4">
{decision.nextActions.map((action, idx) => (
<div key={idx} className="flex gap-4">
<div className="flex-shrink-0 w-8 h-8 bg-purple-100 rounded-full flex items-center justify-center">
<span className="text-sm font-bold text-purple-700">{action.priority}</span>
</div>
<div className="flex-1">
<h4 className="font-semibold text-gray-900 mb-1">{action.title}</h4>
<p className="text-sm text-gray-700 mb-2">{action.description}</p>
<div className="flex items-center gap-3">
{action.effortDays && (
<span className="text-xs text-gray-600">
<span className="font-medium">Aufwand:</span> {action.effortDays} Tage
</span>
)}
{action.relatedDocuments && action.relatedDocuments.length > 0 && (
<span className="text-xs text-gray-600">
<span className="font-medium">Dokumente:</span> {action.relatedDocuments.length}
</span>
)}
</div>
</div>
</div>
))}
</div>
</div>
)}
{/* Audit Trail */}
{decision.auditTrail && decision.auditTrail.length > 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-6">
<button
type="button"
onClick={() => setShowAuditTrail(!showAuditTrail)}
className="w-full flex items-center justify-between mb-4"
>
<h3 className="text-lg font-semibold text-gray-900">Audit-Trail</h3>
<svg
className={`w-5 h-5 text-gray-500 transition-transform ${showAuditTrail ? '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>
</button>
{showAuditTrail && (
<div className="space-y-3">
{decision.auditTrail.map((entry, idx) => (
<div key={idx} className="border-l-2 border-purple-300 pl-4 py-2">
<h4 className="font-medium text-gray-900 mb-1">{entry.step}</h4>
<p className="text-sm text-gray-700 mb-2">{entry.description}</p>
{entry.details && entry.details.length > 0 && (
<ul className="text-xs text-gray-600 space-y-1">
{entry.details.map((detail, detailIdx) => (
<li key={detailIdx}> {detail}</li>
))}
</ul>
)}
</div>
))}
</div>
)}
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,334 @@
'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>
)
}

View File

@@ -0,0 +1,267 @@
'use client'
import React from 'react'
import type { ComplianceScopeState, ScopeDecision, ComplianceDepthLevel } from '@/lib/sdk/compliance-scope-types'
import { DEPTH_LEVEL_LABELS, DEPTH_LEVEL_DESCRIPTIONS, DEPTH_LEVEL_COLORS, DOCUMENT_TYPE_LABELS } from '@/lib/sdk/compliance-scope-types'
interface ScopeOverviewTabProps {
scopeState: ComplianceScopeState
onStartProfiling: () => void
onRefreshDecision: () => void
}
export function ScopeOverviewTab({ scopeState, onStartProfiling, onRefreshDecision }: ScopeOverviewTabProps) {
const { decision, answers } = scopeState
const hasAnswers = answers && answers.length > 0
const getScoreColor = (score: number): string => {
if (score >= 80) return 'from-red-500 to-red-600'
if (score >= 60) return 'from-orange-500 to-orange-600'
if (score >= 40) return 'from-yellow-500 to-yellow-600'
return 'from-green-500 to-green-600'
}
const getScoreColorBg = (score: number): string => {
if (score >= 80) return 'bg-red-100'
if (score >= 60) return 'bg-orange-100'
if (score >= 40) return 'bg-yellow-100'
return 'bg-green-100'
}
const renderScoreGauge = (label: string, score: number | undefined) => {
const value = score ?? 0
return (
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm font-medium text-gray-700">{label}</span>
<span className="text-sm font-bold text-gray-900">{value}/100</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-3 overflow-hidden">
<div
className={`h-full bg-gradient-to-r ${getScoreColor(value)} transition-all duration-500`}
style={{ width: `${value}%` }}
/>
</div>
</div>
)
}
const renderLevelBadge = () => {
if (!decision?.level) {
return (
<div className="bg-gray-100 border border-gray-300 rounded-xl p-8 text-center">
<div className="inline-flex items-center justify-center w-24 h-24 bg-gray-200 rounded-full mb-4">
<span className="text-4xl font-bold text-gray-400">?</span>
</div>
<h3 className="text-xl font-semibold text-gray-600 mb-2">Noch nicht bewertet</h3>
<p className="text-gray-500">
Führen Sie das Scope-Profiling durch, um Ihre Compliance-Tiefe zu bestimmen.
</p>
</div>
)
}
const levelColors = DEPTH_LEVEL_COLORS[decision.level]
return (
<div className={`${levelColors.bg} border-2 ${levelColors.border} rounded-xl p-8 text-center`}>
<div className={`inline-flex items-center justify-center w-24 h-24 ${levelColors.badge} rounded-full mb-4`}>
<span className={`text-4xl font-bold ${levelColors.text}`}>{decision.level}</span>
</div>
<h3 className={`text-xl font-semibold ${levelColors.text} mb-2`}>
{DEPTH_LEVEL_LABELS[decision.level]}
</h3>
<p className="text-gray-600">{DEPTH_LEVEL_DESCRIPTIONS[decision.level]}</p>
</div>
)
}
const renderActiveHardTriggers = () => {
if (!decision?.hardTriggers || decision.hardTriggers.length === 0) {
return null
}
const activeHardTriggers = decision.hardTriggers.filter((ht) => ht.matched)
if (activeHardTriggers.length === 0) {
return null
}
return (
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="flex items-center gap-2 mb-4">
<svg className="w-5 h-5 text-red-600" 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>
<h3 className="text-lg font-semibold text-gray-900">Aktive Hard-Trigger</h3>
</div>
<div className="space-y-3">
{activeHardTriggers.map((trigger, idx) => (
<div
key={idx}
className="border-l-4 border-red-500 bg-red-50 rounded-r-lg p-4"
>
<div className="flex items-start justify-between">
<div className="flex-1">
<h4 className="font-semibold text-gray-900">{trigger.label}</h4>
<p className="text-sm text-gray-600 mt-1">{trigger.description}</p>
{trigger.legalReference && (
<p className="text-xs text-gray-500 mt-2">
<span className="font-medium">Rechtsgrundlage:</span> {trigger.legalReference}
</p>
)}
{trigger.matchedValue && (
<p className="text-xs text-gray-700 mt-1">
<span className="font-medium">Erfasster Wert:</span> {trigger.matchedValue}
</p>
)}
</div>
</div>
</div>
))}
</div>
</div>
)
}
const renderDocumentSummary = () => {
if (!decision?.requiredDocuments) {
return null
}
const mandatoryDocs = decision.requiredDocuments.filter((doc) => doc.isMandatory)
const optionalDocs = decision.requiredDocuments.filter((doc) => !doc.isMandatory)
const totalEffortDays = decision.requiredDocuments.reduce(
(sum, doc) => sum + (doc.effortEstimate?.days ?? 0),
0
)
return (
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Dokumenten-Übersicht</h3>
<div className="grid grid-cols-3 gap-4">
<div className="text-center">
<div className="text-3xl font-bold text-purple-600">{mandatoryDocs.length}</div>
<div className="text-sm text-gray-600 mt-1">Pflichtdokumente</div>
</div>
<div className="text-center">
<div className="text-3xl font-bold text-blue-600">{optionalDocs.length}</div>
<div className="text-sm text-gray-600 mt-1">Optional</div>
</div>
<div className="text-center">
<div className="text-3xl font-bold text-gray-900">{totalEffortDays}</div>
<div className="text-sm text-gray-600 mt-1">Tage Aufwand (geschätzt)</div>
</div>
</div>
</div>
)
}
const renderRiskFlagsSummary = () => {
if (!decision?.riskFlags || decision.riskFlags.length === 0) {
return null
}
const critical = decision.riskFlags.filter((rf) => rf.severity === 'critical').length
const high = decision.riskFlags.filter((rf) => rf.severity === 'high').length
const medium = decision.riskFlags.filter((rf) => rf.severity === 'medium').length
return (
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="flex items-center gap-2 mb-4">
<svg className="w-5 h-5 text-orange-600" 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>
<h3 className="text-lg font-semibold text-gray-900">Risiko-Flags</h3>
</div>
<div className="flex gap-6">
{critical > 0 && (
<div className="flex items-center gap-2">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
Kritisch
</span>
<span className="text-lg font-bold text-red-600">{critical}</span>
</div>
)}
{high > 0 && (
<div className="flex items-center gap-2">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-orange-100 text-orange-800">
Hoch
</span>
<span className="text-lg font-bold text-orange-600">{high}</span>
</div>
)}
{medium > 0 && (
<div className="flex items-center gap-2">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
Mittel
</span>
<span className="text-lg font-bold text-yellow-600">{medium}</span>
</div>
)}
</div>
</div>
)
}
return (
<div className="space-y-6">
{/* Level Badge */}
{renderLevelBadge()}
{/* Scores Section */}
{decision && (
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Score-Übersicht</h3>
<div className="space-y-4">
{renderScoreGauge('Risiko-Score', decision.scores?.riskScore)}
{renderScoreGauge('Komplexitäts-Score', decision.scores?.complexityScore)}
{renderScoreGauge('Assurance-Score', decision.scores?.assuranceScore)}
<div className="pt-4 border-t border-gray-200">
{renderScoreGauge('Gesamt-Score', decision.scores?.compositeScore)}
</div>
</div>
</div>
)}
{/* Active Hard Triggers */}
{renderActiveHardTriggers()}
{/* Document Summary */}
{renderDocumentSummary()}
{/* Risk Flags Summary */}
{renderRiskFlagsSummary()}
{/* CTA Section */}
<div className="bg-gradient-to-r from-purple-50 to-blue-50 rounded-xl border border-purple-200 p-6">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">
{!hasAnswers ? 'Bereit für das Scope-Profiling?' : 'Ergebnis aktualisieren'}
</h3>
<p className="text-gray-600">
{!hasAnswers
? 'Beantworten Sie einige Fragen zu Ihrem Unternehmen und erhalten Sie eine präzise Compliance-Bewertung.'
: 'Haben sich Ihre Unternehmensparameter geändert? Aktualisieren Sie Ihre Bewertung.'}
</p>
</div>
<button
onClick={!hasAnswers ? onStartProfiling : onRefreshDecision}
className="px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors font-medium whitespace-nowrap"
>
{!hasAnswers ? 'Scope-Profiling starten' : 'Ergebnis aktualisieren'}
</button>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,410 @@
'use client'
import React, { useState, useCallback } from 'react'
import type { ScopeProfilingAnswer, ComplianceDepthLevel } from '@/lib/sdk/compliance-scope-types'
import { SCOPE_QUESTION_BLOCKS, getBlockProgress, getTotalProgress, getAnswerValue, getAllQuestions } from '@/lib/sdk/compliance-scope-profiling'
import type { CompanyProfile } from '@/lib/sdk/types'
import { prefillFromCompanyProfile } from '@/lib/sdk/compliance-scope-profiling'
import { DEPTH_LEVEL_LABELS, DEPTH_LEVEL_COLORS } from '@/lib/sdk/compliance-scope-types'
interface ScopeWizardTabProps {
answers: ScopeProfilingAnswer[]
onAnswersChange: (answers: ScopeProfilingAnswer[]) => void
onComplete: () => void
companyProfile: CompanyProfile | null
currentLevel: ComplianceDepthLevel | null
}
export function ScopeWizardTab({
answers,
onAnswersChange,
onComplete,
companyProfile,
currentLevel,
}: ScopeWizardTabProps) {
const [currentBlockIndex, setCurrentBlockIndex] = useState(0)
const currentBlock = SCOPE_QUESTION_BLOCKS[currentBlockIndex]
const totalProgress = getTotalProgress(answers)
const handleAnswerChange = useCallback(
(questionId: string, value: any) => {
const existingIndex = answers.findIndex((a) => a.questionId === questionId)
if (existingIndex >= 0) {
const newAnswers = [...answers]
newAnswers[existingIndex] = { questionId, value, answeredAt: new Date().toISOString() }
onAnswersChange(newAnswers)
} else {
onAnswersChange([...answers, { questionId, value, answeredAt: new Date().toISOString() }])
}
},
[answers, onAnswersChange]
)
const handlePrefillFromProfile = useCallback(() => {
if (!companyProfile) return
const prefilledAnswers = prefillFromCompanyProfile(companyProfile, answers)
onAnswersChange(prefilledAnswers)
}, [companyProfile, answers, onAnswersChange])
const handleNext = useCallback(() => {
if (currentBlockIndex < SCOPE_QUESTION_BLOCKS.length - 1) {
setCurrentBlockIndex(currentBlockIndex + 1)
} else {
onComplete()
}
}, [currentBlockIndex, onComplete])
const handleBack = useCallback(() => {
if (currentBlockIndex > 0) {
setCurrentBlockIndex(currentBlockIndex - 1)
}
}, [currentBlockIndex])
const renderQuestion = (question: any) => {
const currentValue = getAnswerValue(answers, question.id)
switch (question.type) {
case 'boolean':
return (
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="text-sm font-medium text-gray-900">
{question.question}
{question.required && <span className="text-red-500 ml-1">*</span>}
</label>
{question.helpText && (
<button
type="button"
className="text-gray-400 hover:text-gray-600"
title={question.helpText}
>
<svg className="w-5 h-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>
</button>
)}
</div>
<div className="flex gap-3">
<button
type="button"
onClick={() => handleAnswerChange(question.id, true)}
className={`flex-1 py-2 px-4 rounded-lg border-2 transition-all ${
currentValue === true
? 'border-purple-500 bg-purple-50 text-purple-700 font-medium'
: 'border-gray-300 bg-white text-gray-700 hover:border-gray-400'
}`}
>
Ja
</button>
<button
type="button"
onClick={() => handleAnswerChange(question.id, false)}
className={`flex-1 py-2 px-4 rounded-lg border-2 transition-all ${
currentValue === false
? 'border-purple-500 bg-purple-50 text-purple-700 font-medium'
: 'border-gray-300 bg-white text-gray-700 hover:border-gray-400'
}`}
>
Nein
</button>
</div>
</div>
)
case 'single':
return (
<div className="space-y-2">
<label className="text-sm font-medium text-gray-900">
{question.question}
{question.required && <span className="text-red-500 ml-1">*</span>}
{question.helpText && (
<button
type="button"
className="ml-2 text-gray-400 hover:text-gray-600 inline"
title={question.helpText}
>
<svg className="w-4 h-4 inline" 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>
</button>
)}
</label>
<div className="space-y-2">
{question.options?.map((option: any) => (
<button
key={option.value}
type="button"
onClick={() => handleAnswerChange(question.id, option.value)}
className={`w-full text-left py-3 px-4 rounded-lg border-2 transition-all ${
currentValue === option.value
? 'border-purple-500 bg-purple-50 text-purple-700 font-medium'
: 'border-gray-300 bg-white text-gray-700 hover:border-gray-400'
}`}
>
{option.label}
</button>
))}
</div>
</div>
)
case 'multi':
return (
<div className="space-y-2">
<label className="text-sm font-medium text-gray-900">
{question.question}
{question.required && <span className="text-red-500 ml-1">*</span>}
{question.helpText && (
<button
type="button"
className="ml-2 text-gray-400 hover:text-gray-600 inline"
title={question.helpText}
>
<svg className="w-4 h-4 inline" 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>
</button>
)}
</label>
<div className="space-y-2">
{question.options?.map((option: any) => {
const selectedValues = Array.isArray(currentValue) ? currentValue : []
const isChecked = selectedValues.includes(option.value)
return (
<label
key={option.value}
className={`flex items-center gap-3 py-3 px-4 rounded-lg border-2 cursor-pointer transition-all ${
isChecked
? 'border-purple-500 bg-purple-50'
: 'border-gray-300 bg-white hover:border-gray-400'
}`}
>
<input
type="checkbox"
checked={isChecked}
onChange={(e) => {
const newValues = e.target.checked
? [...selectedValues, option.value]
: selectedValues.filter((v) => v !== option.value)
handleAnswerChange(question.id, newValues)
}}
className="w-5 h-5 text-purple-600 border-gray-300 rounded focus:ring-purple-500"
/>
<span className={isChecked ? 'text-purple-700 font-medium' : 'text-gray-700'}>
{option.label}
</span>
</label>
)
})}
</div>
</div>
)
case 'number':
return (
<div className="space-y-2">
<label className="text-sm font-medium text-gray-900">
{question.question}
{question.required && <span className="text-red-500 ml-1">*</span>}
{question.helpText && (
<button
type="button"
className="ml-2 text-gray-400 hover:text-gray-600 inline"
title={question.helpText}
>
<svg className="w-4 h-4 inline" 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>
</button>
)}
</label>
<input
type="number"
value={currentValue ?? ''}
onChange={(e) => handleAnswerChange(question.id, parseInt(e.target.value, 10))}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
placeholder="Zahl eingeben"
/>
</div>
)
case 'text':
return (
<div className="space-y-2">
<label className="text-sm font-medium text-gray-900">
{question.question}
{question.required && <span className="text-red-500 ml-1">*</span>}
{question.helpText && (
<button
type="button"
className="ml-2 text-gray-400 hover:text-gray-600 inline"
title={question.helpText}
>
<svg className="w-4 h-4 inline" 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>
</button>
)}
</label>
<input
type="text"
value={currentValue ?? ''}
onChange={(e) => handleAnswerChange(question.id, e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
placeholder="Text eingeben"
/>
</div>
)
default:
return null
}
}
return (
<div className="flex gap-6 h-full">
{/* Left Sidebar - Block Navigation */}
<div className="w-64 flex-shrink-0">
<div className="bg-white rounded-xl border border-gray-200 p-4 sticky top-4">
<h3 className="text-sm font-semibold text-gray-900 mb-3">Fortschritt</h3>
<div className="space-y-2">
{SCOPE_QUESTION_BLOCKS.map((block, idx) => {
const progress = getBlockProgress(answers, block.id)
const isActive = idx === currentBlockIndex
return (
<button
key={block.id}
type="button"
onClick={() => setCurrentBlockIndex(idx)}
className={`w-full text-left px-3 py-2 rounded-lg transition-all ${
isActive
? 'bg-purple-50 border-2 border-purple-500'
: 'bg-gray-50 border border-gray-200 hover:border-gray-300'
}`}
>
<div className="flex items-center justify-between mb-1">
<span className={`text-sm font-medium ${isActive ? 'text-purple-700' : 'text-gray-700'}`}>
{block.title}
</span>
<span className={`text-xs font-semibold ${isActive ? 'text-purple-600' : 'text-gray-500'}`}>
{progress}%
</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-1.5 overflow-hidden">
<div
className={`h-full transition-all ${isActive ? 'bg-purple-500' : 'bg-gray-400'}`}
style={{ width: `${progress}%` }}
/>
</div>
</button>
)
})}
</div>
</div>
</div>
{/* Main Content Area */}
<div className="flex-1 space-y-6">
{/* Progress Bar */}
<div className="bg-white rounded-xl border border-gray-200 p-4">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-gray-700">Gesamtfortschritt</span>
<div className="flex items-center gap-3">
{currentLevel && (
<div className="flex items-center gap-2">
<span className="text-xs text-gray-500">Vorläufige Einstufung:</span>
<span
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-bold ${DEPTH_LEVEL_COLORS[currentLevel].badge} ${DEPTH_LEVEL_COLORS[currentLevel].text}`}
>
{currentLevel} - {DEPTH_LEVEL_LABELS[currentLevel]}
</span>
</div>
)}
<span className="text-sm font-bold text-gray-900">{totalProgress}%</span>
</div>
</div>
<div className="w-full bg-gray-200 rounded-full h-3 overflow-hidden">
<div
className="h-full bg-gradient-to-r from-purple-500 to-blue-500 transition-all duration-500"
style={{ width: `${totalProgress}%` }}
/>
</div>
</div>
{/* Current Block */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="flex items-start justify-between mb-6">
<div>
<h2 className="text-2xl font-bold text-gray-900 mb-2">{currentBlock.title}</h2>
<p className="text-gray-600">{currentBlock.description}</p>
</div>
{companyProfile && (
<button
type="button"
onClick={handlePrefillFromProfile}
className="px-4 py-2 text-sm bg-blue-50 text-blue-700 border border-blue-300 rounded-lg hover:bg-blue-100 transition-colors whitespace-nowrap"
>
Aus Unternehmensprofil übernehmen
</button>
)}
</div>
{/* Questions */}
<div className="space-y-6">
{currentBlock.questions.map((question) => (
<div key={question.id} className="border-b border-gray-100 pb-6 last:border-b-0 last:pb-0">
{renderQuestion(question)}
</div>
))}
</div>
</div>
{/* Navigation Buttons */}
<div className="flex items-center justify-between bg-white rounded-xl border border-gray-200 p-4">
<button
type="button"
onClick={handleBack}
disabled={currentBlockIndex === 0}
className="px-6 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
Zurück
</button>
<span className="text-sm text-gray-600">
Block {currentBlockIndex + 1} von {SCOPE_QUESTION_BLOCKS.length}
</span>
<button
type="button"
onClick={handleNext}
className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors font-medium"
>
{currentBlockIndex === SCOPE_QUESTION_BLOCKS.length - 1 ? 'Auswertung starten' : 'Weiter'}
</button>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,4 @@
export { ScopeOverviewTab } from './ScopeOverviewTab'
export { ScopeWizardTab } from './ScopeWizardTab'
export { ScopeDecisionTab } from './ScopeDecisionTab'
export { ScopeExportTab } from './ScopeExportTab'