refactor(admin): split StepHeader, SDKSidebar, ScopeWizardTab, PIIRulesTab, ReviewExportStep, DocumentUploadSection components

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Sharang Parnerkar
2026-04-17 12:32:45 +02:00
parent e3a1822883
commit d32ad81094
18 changed files with 2134 additions and 3046 deletions

View File

@@ -0,0 +1,40 @@
'use client'
// =============================================================================
// DocumentUpload icon components — extracted from DocumentUploadSection for LOC compliance
// =============================================================================
export const UploadIcon = () => (
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
)
export const QRIcon = () => (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v1m6 11h2m-6 0h-2v4m0-11v3m0 0h.01M12 12h4.01M16 20h4M4 12h4m12 0h.01M5 8h2a1 1 0 001-1V5a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1zm12 0h2a1 1 0 001-1V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v2a1 1 0 001 1zM5 20h2a1 1 0 001-1v-2a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1z" />
</svg>
)
export const DocumentIcon = () => (
<svg className="w-5 h-5" 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>
)
export const CheckIcon = () => (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
)
export const EditIcon = () => (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
)
export const CloseIcon = () => (
<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>
)

View File

@@ -0,0 +1,123 @@
'use client'
// =============================================================================
// QRCodeModal — extracted from DocumentUploadSection for LOC compliance
// =============================================================================
import { useEffect, useState } from 'react'
import { QRIcon, CloseIcon } from './DocumentUploadIcons'
interface QRModalProps {
isOpen: boolean
onClose: () => void
sessionId: string
onFileUploaded?: (file: File) => void
}
export function QRCodeModal({ isOpen, onClose, sessionId }: QRModalProps) {
const [uploadUrl, setUploadUrl] = useState('')
const [qrCodeUrl, setQrCodeUrl] = useState<string | null>(null)
useEffect(() => {
if (!isOpen) return
let baseUrl = typeof window !== 'undefined' ? window.location.origin : ''
// Hostname to IP mapping for local network
const hostnameToIP: Record<string, string> = {
'macmini': '192.168.178.100',
'macmini.local': '192.168.178.100',
}
Object.entries(hostnameToIP).forEach(([hostname, ip]) => {
if (baseUrl.includes(hostname)) {
baseUrl = baseUrl.replace(hostname, ip)
}
})
// Force HTTP for mobile access (SSL cert is for hostname, not IP)
// This is safe because it's only used on the local network
if (baseUrl.startsWith('https://')) {
baseUrl = baseUrl.replace('https://', 'http://')
}
const uploadPath = `/upload/sdk/${sessionId}`
const fullUrl = `${baseUrl}${uploadPath}`
setUploadUrl(fullUrl)
const qrApiUrl = `https://api.qrserver.com/v1/create-qr-code/?size=250x250&data=${encodeURIComponent(fullUrl)}`
setQrCodeUrl(qrApiUrl)
}, [isOpen, sessionId])
const copyToClipboard = async () => {
try {
await navigator.clipboard.writeText(uploadUrl)
} catch (err) {
console.error('Copy failed:', err)
}
}
if (!isOpen) return null
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className="absolute inset-0 bg-black/50 backdrop-blur-sm" onClick={onClose} />
<div className="relative bg-white rounded-2xl shadow-xl max-w-md w-full p-6">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-purple-100 flex items-center justify-center">
<QRIcon />
</div>
<div>
<h3 className="font-semibold text-gray-900">Mit Handy hochladen</h3>
<p className="text-sm text-gray-500">QR-Code scannen</p>
</div>
</div>
<button onClick={onClose} className="p-2 hover:bg-gray-100 rounded-lg">
<CloseIcon />
</button>
</div>
<div className="flex flex-col items-center">
<div className="p-4 bg-white border border-gray-200 rounded-xl">
{qrCodeUrl ? (
<img src={qrCodeUrl} alt="QR Code" className="w-[200px] h-[200px]" />
) : (
<div className="w-[200px] h-[200px] flex items-center justify-center">
<div className="w-8 h-8 border-2 border-purple-500 border-t-transparent rounded-full animate-spin" />
</div>
)}
</div>
<p className="mt-4 text-center text-sm text-gray-600">
Scannen Sie den Code mit Ihrem Handy,<br />
um Dokumente hochzuladen.
</p>
<div className="mt-4 w-full">
<p className="text-xs text-gray-400 mb-2">Oder Link teilen:</p>
<div className="flex gap-2">
<input
type="text"
value={uploadUrl}
readOnly
className="flex-1 px-3 py-2 text-sm border border-gray-200 rounded-lg bg-gray-50"
/>
<button
onClick={copyToClipboard}
className="px-4 py-2 text-sm bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
>
Kopieren
</button>
</div>
</div>
<div className="mt-4 p-3 bg-amber-50 border border-amber-200 rounded-lg w-full">
<p className="text-xs text-amber-800">
<strong>Hinweis:</strong> Ihr Handy muss im gleichen Netzwerk sein.
</p>
</div>
</div>
</div>
</div>
)
}

View File

@@ -1,257 +1,30 @@
'use client'
import React, { useState, useCallback, useEffect } from 'react'
import React, { useState, useCallback } from 'react'
import { useRouter } from 'next/navigation'
import {
type UploadedDocument,
type DocumentUploadSectionProps,
formatFileSize,
detectVersionFromFilename,
suggestNextVersion,
} from './DocumentUploadTypes'
import {
UploadIcon,
QRIcon,
DocumentIcon,
CheckIcon,
EditIcon,
CloseIcon,
} from './DocumentUploadIcons'
import { QRCodeModal } from './DocumentUploadQRModal'
// =============================================================================
// TYPES
// =============================================================================
export interface UploadedDocument {
id: string
name: string
type: string
size: number
uploadedAt: Date
extractedVersion?: string
extractedContent?: ExtractedContent
status: 'uploading' | 'processing' | 'ready' | 'error'
error?: string
}
export interface ExtractedContent {
title?: string
version?: string
lastModified?: string
sections?: ExtractedSection[]
metadata?: Record<string, string>
}
export interface ExtractedSection {
title: string
content: string
type?: string
}
export interface DocumentUploadSectionProps {
/** Type of document being uploaded (tom, dsfa, vvt, loeschfristen, etc.) */
documentType: 'tom' | 'dsfa' | 'vvt' | 'loeschfristen' | 'consent' | 'policy' | 'custom'
/** Title displayed in the upload section */
title?: string
/** Description text */
description?: string
/** Accepted file types */
acceptedTypes?: string
/** Callback when document is uploaded and processed */
onDocumentProcessed?: (doc: UploadedDocument) => void
/** Callback to open document in workflow editor */
onOpenInEditor?: (doc: UploadedDocument) => void
/** Session ID for QR upload */
sessionId?: string
/** Custom CSS classes */
className?: string
}
// =============================================================================
// ICONS
// =============================================================================
const UploadIcon = () => (
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
)
const QRIcon = () => (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v1m6 11h2m-6 0h-2v4m0-11v3m0 0h.01M12 12h4.01M16 20h4M4 12h4m12 0h.01M5 8h2a1 1 0 001-1V5a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1zm12 0h2a1 1 0 001-1V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v2a1 1 0 001 1zM5 20h2a1 1 0 001-1v-2a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1z" />
</svg>
)
const DocumentIcon = () => (
<svg className="w-5 h-5" 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>
)
const CheckIcon = () => (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
)
const EditIcon = () => (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
)
const CloseIcon = () => (
<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>
)
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
function formatFileSize(bytes: number): string {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
}
function detectVersionFromFilename(filename: string): string | undefined {
// Common version patterns: v1.0, V2.1, _v3, -v1.2.3, version-2
const patterns = [
/[vV](\d+(?:\.\d+)*)/,
/version[_-]?(\d+(?:\.\d+)*)/i,
/[_-]v?(\d+\.\d+(?:\.\d+)?)[_-]/,
]
for (const pattern of patterns) {
const match = filename.match(pattern)
if (match) {
return match[1]
}
}
return undefined
}
function suggestNextVersion(currentVersion?: string): string {
if (!currentVersion) return '1.0'
const parts = currentVersion.split('.').map(Number)
if (parts.length >= 2) {
parts[parts.length - 1] += 1
} else {
parts.push(1)
}
return parts.join('.')
}
// =============================================================================
// QR CODE MODAL
// =============================================================================
interface QRModalProps {
isOpen: boolean
onClose: () => void
sessionId: string
onFileUploaded?: (file: File) => void
}
function QRCodeModal({ isOpen, onClose, sessionId }: QRModalProps) {
const [uploadUrl, setUploadUrl] = useState('')
const [qrCodeUrl, setQrCodeUrl] = useState<string | null>(null)
useEffect(() => {
if (!isOpen) return
let baseUrl = typeof window !== 'undefined' ? window.location.origin : ''
// Hostname to IP mapping for local network
const hostnameToIP: Record<string, string> = {
'macmini': '192.168.178.100',
'macmini.local': '192.168.178.100',
}
Object.entries(hostnameToIP).forEach(([hostname, ip]) => {
if (baseUrl.includes(hostname)) {
baseUrl = baseUrl.replace(hostname, ip)
}
})
// Force HTTP for mobile access (SSL cert is for hostname, not IP)
// This is safe because it's only used on the local network
if (baseUrl.startsWith('https://')) {
baseUrl = baseUrl.replace('https://', 'http://')
}
const uploadPath = `/upload/sdk/${sessionId}`
const fullUrl = `${baseUrl}${uploadPath}`
setUploadUrl(fullUrl)
const qrApiUrl = `https://api.qrserver.com/v1/create-qr-code/?size=250x250&data=${encodeURIComponent(fullUrl)}`
setQrCodeUrl(qrApiUrl)
}, [isOpen, sessionId])
const copyToClipboard = async () => {
try {
await navigator.clipboard.writeText(uploadUrl)
} catch (err) {
console.error('Copy failed:', err)
}
}
if (!isOpen) return null
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className="absolute inset-0 bg-black/50 backdrop-blur-sm" onClick={onClose} />
<div className="relative bg-white rounded-2xl shadow-xl max-w-md w-full p-6">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-purple-100 flex items-center justify-center">
<QRIcon />
</div>
<div>
<h3 className="font-semibold text-gray-900">Mit Handy hochladen</h3>
<p className="text-sm text-gray-500">QR-Code scannen</p>
</div>
</div>
<button onClick={onClose} className="p-2 hover:bg-gray-100 rounded-lg">
<CloseIcon />
</button>
</div>
<div className="flex flex-col items-center">
<div className="p-4 bg-white border border-gray-200 rounded-xl">
{qrCodeUrl ? (
<img src={qrCodeUrl} alt="QR Code" className="w-[200px] h-[200px]" />
) : (
<div className="w-[200px] h-[200px] flex items-center justify-center">
<div className="w-8 h-8 border-2 border-purple-500 border-t-transparent rounded-full animate-spin" />
</div>
)}
</div>
<p className="mt-4 text-center text-sm text-gray-600">
Scannen Sie den Code mit Ihrem Handy,<br />
um Dokumente hochzuladen.
</p>
<div className="mt-4 w-full">
<p className="text-xs text-gray-400 mb-2">Oder Link teilen:</p>
<div className="flex gap-2">
<input
type="text"
value={uploadUrl}
readOnly
className="flex-1 px-3 py-2 text-sm border border-gray-200 rounded-lg bg-gray-50"
/>
<button
onClick={copyToClipboard}
className="px-4 py-2 text-sm bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
>
Kopieren
</button>
</div>
</div>
<div className="mt-4 p-3 bg-amber-50 border border-amber-200 rounded-lg w-full">
<p className="text-xs text-amber-800">
<strong>Hinweis:</strong> Ihr Handy muss im gleichen Netzwerk sein.
</p>
</div>
</div>
</div>
</div>
)
}
export type {
UploadedDocument,
ExtractedContent,
ExtractedSection,
DocumentUploadSectionProps,
} from './DocumentUploadTypes'
// =============================================================================
// MAIN COMPONENT

View File

@@ -0,0 +1,89 @@
// =============================================================================
// DocumentUpload shared types — extracted from DocumentUploadSection for LOC compliance
// =============================================================================
export interface UploadedDocument {
id: string
name: string
type: string
size: number
uploadedAt: Date
extractedVersion?: string
extractedContent?: ExtractedContent
status: 'uploading' | 'processing' | 'ready' | 'error'
error?: string
}
export interface ExtractedContent {
title?: string
version?: string
lastModified?: string
sections?: ExtractedSection[]
metadata?: Record<string, string>
}
export interface ExtractedSection {
title: string
content: string
type?: string
}
export interface DocumentUploadSectionProps {
/** Type of document being uploaded (tom, dsfa, vvt, loeschfristen, etc.) */
documentType: 'tom' | 'dsfa' | 'vvt' | 'loeschfristen' | 'consent' | 'policy' | 'custom'
/** Title displayed in the upload section */
title?: string
/** Description text */
description?: string
/** Accepted file types */
acceptedTypes?: string
/** Callback when document is uploaded and processed */
onDocumentProcessed?: (doc: UploadedDocument) => void
/** Callback to open document in workflow editor */
onOpenInEditor?: (doc: UploadedDocument) => void
/** Session ID for QR upload */
sessionId?: string
/** Custom CSS classes */
className?: string
}
// =============================================================================
// Helper functions
// =============================================================================
export function formatFileSize(bytes: number): string {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
}
export function detectVersionFromFilename(filename: string): string | undefined {
// Common version patterns: v1.0, V2.1, _v3, -v1.2.3, version-2
const patterns = [
/[vV](\d+(?:\.\d+)*)/,
/version[_-]?(\d+(?:\.\d+)*)/i,
/[_-]v?(\d+\.\d+(?:\.\d+)?)[_-]/,
]
for (const pattern of patterns) {
const match = filename.match(pattern)
if (match) {
return match[1]
}
}
return undefined
}
export function suggestNextVersion(currentVersion?: string): string {
if (!currentVersion) return '1.0'
const parts = currentVersion.split('.').map(Number)
if (parts.length >= 2) {
parts[parts.length - 1] += 1
} else {
parts.push(1)
}
return parts.join('.')
}

View File

@@ -10,301 +10,16 @@ import {
getStepsForPackage,
type SDKPackageId,
type SDKStep,
type RAGCorpusStatus,
} from '@/lib/sdk'
/**
* Append ?project= to a URL if a projectId is set
*/
function withProject(url: string, projectId?: string): string {
if (!projectId) return url
const separator = url.includes('?') ? '&' : '?'
return `${url}${separator}project=${projectId}`
}
// =============================================================================
// ICONS
// =============================================================================
const CheckIcon = () => (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
)
const LockIcon = () => (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
)
const WarningIcon = () => (
<svg className="w-4 h-4" 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>
)
const ChevronDownIcon = ({ className = '' }: { className?: string }) => (
<svg className={`w-4 h-4 ${className}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
)
const CollapseIcon = ({ collapsed }: { collapsed: boolean }) => (
<svg
className={`w-5 h-5 transition-transform duration-300 ${collapsed ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 19l-7-7 7-7m8 14l-7-7 7-7" />
</svg>
)
// =============================================================================
// PROGRESS BAR
// =============================================================================
interface ProgressBarProps {
value: number
className?: string
}
function ProgressBar({ value, className = '' }: ProgressBarProps) {
return (
<div className={`h-1 bg-gray-200 rounded-full overflow-hidden ${className}`}>
<div
className="h-full bg-purple-600 rounded-full transition-all duration-500"
style={{ width: `${Math.min(100, Math.max(0, value))}%` }}
/>
</div>
)
}
// =============================================================================
// PACKAGE INDICATOR
// =============================================================================
interface PackageIndicatorProps {
packageId: SDKPackageId
order: number
name: string
icon: string
completion: number
isActive: boolean
isExpanded: boolean
isLocked: boolean
onToggle: () => void
collapsed: boolean
}
function PackageIndicator({
order,
name,
icon,
completion,
isActive,
isExpanded,
isLocked,
onToggle,
collapsed,
}: PackageIndicatorProps) {
if (collapsed) {
return (
<button
onClick={onToggle}
className={`w-full flex items-center justify-center py-3 transition-colors ${
isActive
? 'bg-purple-50 border-l-4 border-purple-600'
: isLocked
? 'border-l-4 border-transparent opacity-50'
: 'hover:bg-gray-50 border-l-4 border-transparent'
}`}
title={`${order}. ${name} (${completion}%)`}
>
<div
className={`w-8 h-8 rounded-full flex items-center justify-center text-lg ${
isLocked
? 'bg-gray-200 text-gray-400'
: isActive
? 'bg-purple-600 text-white'
: completion === 100
? 'bg-green-500 text-white'
: 'bg-gray-200 text-gray-600'
}`}
>
{isLocked ? <LockIcon /> : completion === 100 ? <CheckIcon /> : icon}
</div>
</button>
)
}
return (
<button
onClick={onToggle}
disabled={isLocked}
className={`w-full flex items-center justify-between px-4 py-3 text-left transition-colors ${
isLocked
? 'opacity-50 cursor-not-allowed'
: isActive
? 'bg-purple-50 border-l-4 border-purple-600'
: 'hover:bg-gray-50 border-l-4 border-transparent'
}`}
>
<div className="flex items-center gap-3">
<div
className={`w-8 h-8 rounded-full flex items-center justify-center text-lg ${
isLocked
? 'bg-gray-200 text-gray-400'
: isActive
? 'bg-purple-600 text-white'
: completion === 100
? 'bg-green-500 text-white'
: 'bg-gray-200 text-gray-600'
}`}
>
{isLocked ? <LockIcon /> : completion === 100 ? <CheckIcon /> : icon}
</div>
<div>
<div className={`font-medium text-sm ${isActive ? 'text-purple-900' : isLocked ? 'text-gray-400' : 'text-gray-700'}`}>
{order}. {name}
</div>
<div className="text-xs text-gray-500">{completion}%</div>
</div>
</div>
{!isLocked && <ChevronDownIcon className={`transition-transform ${isExpanded ? 'rotate-180' : ''}`} />}
</button>
)
}
// =============================================================================
// STEP ITEM
// =============================================================================
interface StepItemProps {
step: SDKStep
isActive: boolean
isCompleted: boolean
isLocked: boolean
checkpointStatus?: 'passed' | 'failed' | 'warning' | 'pending'
collapsed: boolean
projectId?: string
}
function StepItem({ step, isActive, isCompleted, isLocked, checkpointStatus, collapsed, projectId }: StepItemProps) {
const content = (
<div
className={`flex items-center gap-3 px-4 py-2.5 text-sm transition-colors ${
collapsed ? 'justify-center' : ''
} ${
isActive
? 'bg-purple-100 text-purple-900 font-medium'
: isLocked
? 'text-gray-400 cursor-not-allowed'
: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'
}`}
title={collapsed ? step.name : undefined}
>
{/* Step indicator */}
<div className="flex-shrink-0">
{isCompleted ? (
<div className="w-5 h-5 rounded-full bg-green-500 text-white flex items-center justify-center">
<CheckIcon />
</div>
) : isLocked ? (
<div className="w-5 h-5 rounded-full bg-gray-200 text-gray-400 flex items-center justify-center">
<LockIcon />
</div>
) : isActive ? (
<div className="w-5 h-5 rounded-full bg-purple-600 flex items-center justify-center">
<div className="w-2 h-2 rounded-full bg-white" />
</div>
) : (
<div className="w-5 h-5 rounded-full border-2 border-gray-300" />
)}
</div>
{/* Step name - hidden when collapsed */}
{!collapsed && <span className="flex-1 truncate">{step.nameShort}</span>}
{/* Checkpoint status - hidden when collapsed */}
{!collapsed && checkpointStatus && checkpointStatus !== 'pending' && (
<div className="flex-shrink-0">
{checkpointStatus === 'passed' ? (
<div className="w-4 h-4 rounded-full bg-green-100 text-green-600 flex items-center justify-center">
<CheckIcon />
</div>
) : checkpointStatus === 'failed' ? (
<div className="w-4 h-4 rounded-full bg-red-100 text-red-600 flex items-center justify-center">
<span className="text-xs font-bold">!</span>
</div>
) : (
<div className="w-4 h-4 rounded-full bg-yellow-100 text-yellow-600 flex items-center justify-center">
<WarningIcon />
</div>
)}
</div>
)}
</div>
)
if (isLocked) {
return content
}
return (
<Link href={withProject(step.url, projectId)} className="block">
{content}
</Link>
)
}
// =============================================================================
// ADDITIONAL MODULE ITEM
// =============================================================================
interface AdditionalModuleItemProps {
href: string
icon: React.ReactNode
label: string
isActive: boolean
collapsed: boolean
projectId?: string
}
function AdditionalModuleItem({ href, icon, label, isActive, collapsed, projectId }: AdditionalModuleItemProps) {
const isExternal = href.startsWith('http')
const className = `flex items-center gap-3 px-4 py-2.5 text-sm transition-colors ${
collapsed ? 'justify-center' : ''
} ${
isActive
? 'bg-purple-100 text-purple-900 font-medium'
: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'
}`
if (isExternal) {
return (
<a href={href} target="_blank" rel="noopener noreferrer" className={className} title={collapsed ? label : undefined}>
{icon}
{!collapsed && (
<span className="flex items-center gap-1">
{label}
<svg className="w-3 h-3 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</span>
)}
</a>
)
}
return (
<Link href={withProject(href, projectId)} className={className} title={collapsed ? label : undefined}>
{icon}
{!collapsed && <span>{label}</span>}
</Link>
)
}
import { CollapseIcon } from './SidebarIcons'
import {
ProgressBar,
PackageIndicator,
StepItem,
CorpusStalenessInfo,
withProject,
} from './SidebarSubComponents'
import { SidebarModuleList } from './SidebarModuleList'
// =============================================================================
// MAIN SIDEBAR
@@ -315,41 +30,6 @@ interface SDKSidebarProps {
onCollapsedChange?: (collapsed: boolean) => void
}
// =============================================================================
// CORPUS STALENESS INFO
// =============================================================================
function CorpusStalenessInfo({ ragCorpusStatus }: { ragCorpusStatus: RAGCorpusStatus }) {
const collections = ragCorpusStatus.collections
const collectionNames = Object.keys(collections)
if (collectionNames.length === 0) return null
// Check if corpus was updated after the last fetch (simplified: show last update time)
const lastUpdated = collectionNames.reduce((latest, name) => {
const updated = new Date(collections[name].last_updated)
return updated > latest ? updated : latest
}, new Date(0))
const daysSinceUpdate = Math.floor((Date.now() - lastUpdated.getTime()) / (1000 * 60 * 60 * 24))
const totalChunks = collectionNames.reduce((sum, name) => sum + collections[name].chunks_count, 0)
return (
<div className="px-4 py-2 border-b border-gray-100">
<div className="flex items-center gap-2 text-xs">
<div className={`w-2 h-2 rounded-full flex-shrink-0 ${daysSinceUpdate > 30 ? 'bg-amber-400' : 'bg-green-400'}`} />
<span className="text-gray-500 truncate">
RAG Corpus: {totalChunks} Chunks
</span>
</div>
{daysSinceUpdate > 30 && (
<div className="mt-1 text-xs text-amber-600 bg-amber-50 rounded px-2 py-1">
Corpus {daysSinceUpdate}d alt Re-Evaluation empfohlen
</div>
)}
</div>
)
}
export function SDKSidebar({ collapsed = false, onCollapsedChange }: SDKSidebarProps) {
const pathname = usePathname()
const { state, packageCompletion, completionPercentage, getCheckpointStatus, projectId } = useSDK()
@@ -404,11 +84,8 @@ export function SDKSidebar({ collapsed = false, onCollapsedChange }: SDKSidebarP
if (state.preferences?.allowParallelWork) return false
const pkg = SDK_PACKAGES.find(p => p.id === packageId)
if (!pkg || pkg.order === 1) return false
// Check if previous package is complete
const prevPkg = SDK_PACKAGES.find(p => p.order === pkg.order - 1)
if (!prevPkg) return false
return packageCompletion[prevPkg.id] < 100
}
@@ -428,7 +105,6 @@ export function SDKSidebar({ collapsed = false, onCollapsedChange }: SDKSidebarP
return steps.some(s => s.url === pathname)
}
// Filter steps based on visibleWhen conditions
const getVisibleStepsForPackage = (packageId: SDKPackageId): SDKStep[] => {
const steps = getStepsForPackage(packageId)
return steps.filter(step => {
@@ -448,12 +124,7 @@ export function SDKSidebar({ collapsed = false, onCollapsedChange }: SDKSidebarP
>
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-purple-600 to-indigo-600 flex items-center justify-center flex-shrink-0">
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
/>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
</div>
{!collapsed && (
@@ -467,7 +138,7 @@ export function SDKSidebar({ collapsed = false, onCollapsedChange }: SDKSidebarP
</Link>
</div>
{/* Overall Progress - hidden when collapsed */}
{/* Overall Progress */}
{!collapsed && (
<div className="px-4 py-3 border-b border-gray-100">
<div className="flex items-center justify-between text-sm mb-2">
@@ -483,7 +154,7 @@ export function SDKSidebar({ collapsed = false, onCollapsedChange }: SDKSidebarP
<CorpusStalenessInfo ragCorpusStatus={state.ragCorpusStatus} />
)}
{/* Navigation - 5 Packages */}
{/* Navigation 5 Packages */}
<nav className="flex-1 overflow-y-auto">
{SDK_PACKAGES.map(pkg => {
const steps = getVisibleStepsForPackage(pkg.id)
@@ -524,368 +195,15 @@ export function SDKSidebar({ collapsed = false, onCollapsedChange }: SDKSidebarP
)
})}
{/* Maschinenrecht / CE */}
<div className="border-t border-gray-100 py-2">
{!collapsed && (
<div className="px-4 py-2 text-xs font-medium text-gray-400 uppercase tracking-wider">
Maschinenrecht / CE
</div>
)}
<AdditionalModuleItem
href="/sdk/iace"
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
</svg>
}
label="CE-Compliance (IACE)"
isActive={pathname?.startsWith('/sdk/iace') ?? false}
collapsed={collapsed}
projectId={projectId}
/>
</div>
{/* Additional Modules */}
<div className="border-t border-gray-100 py-2">
{!collapsed && (
<div className="px-4 py-2 text-xs font-medium text-gray-400 uppercase tracking-wider">
Zusatzmodule
</div>
)}
<AdditionalModuleItem
href="/sdk/training"
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
</svg>
}
label="Schulung (Admin)"
isActive={pathname === '/sdk/training'}
collapsed={collapsed}
projectId={projectId}
/>
<AdditionalModuleItem
href="/sdk/training/learner"
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
}
label="Schulung (Learner)"
isActive={pathname === '/sdk/training/learner'}
collapsed={collapsed}
projectId={projectId}
/>
<AdditionalModuleItem
href="/sdk/rag"
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
}
label="Legal RAG"
isActive={pathname === '/sdk/rag'}
collapsed={collapsed}
projectId={projectId}
/>
<AdditionalModuleItem
href="/sdk/quality"
icon={
<svg className="w-5 h-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>
}
label="AI Quality"
isActive={pathname === '/sdk/quality'}
collapsed={collapsed}
projectId={projectId}
/>
<AdditionalModuleItem
href="/sdk/security-backlog"
icon={
<svg className="w-5 h-5" 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>
}
label="Security Backlog"
isActive={pathname === '/sdk/security-backlog'}
collapsed={collapsed}
projectId={projectId}
/>
<AdditionalModuleItem
href="/sdk/compliance-hub"
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
}
label="Compliance Hub"
isActive={pathname === '/sdk/compliance-hub'}
collapsed={collapsed}
projectId={projectId}
/>
<AdditionalModuleItem
href="/sdk/assertions"
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
</svg>
}
label="Assertions"
isActive={pathname === '/sdk/assertions'}
collapsed={collapsed}
projectId={projectId}
/>
<AdditionalModuleItem
href="/sdk/dsms"
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
}
label="DSMS"
isActive={pathname === '/sdk/dsms'}
collapsed={collapsed}
projectId={projectId}
/>
<AdditionalModuleItem
href="/sdk/sdk-flow"
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
}
label="SDK Flow"
isActive={pathname === '/sdk/sdk-flow'}
collapsed={collapsed}
projectId={projectId}
/>
<AdditionalModuleItem
href="/sdk/architecture"
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01" />
</svg>
}
label="Architektur"
isActive={pathname === '/sdk/architecture'}
collapsed={collapsed}
projectId={projectId}
/>
<AdditionalModuleItem
href="/sdk/agents"
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
}
label="Agenten"
isActive={pathname?.startsWith('/sdk/agents') ?? false}
collapsed={collapsed}
projectId={projectId}
/>
<AdditionalModuleItem
href="/sdk/workshop"
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
}
label="Workshop"
isActive={pathname === '/sdk/workshop'}
collapsed={collapsed}
projectId={projectId}
/>
<AdditionalModuleItem
href="/sdk/portfolio"
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
</svg>
}
label="Portfolio"
isActive={pathname === '/sdk/portfolio'}
collapsed={collapsed}
projectId={projectId}
/>
<AdditionalModuleItem
href="/sdk/roadmap"
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" />
</svg>
}
label="Roadmap"
isActive={pathname === '/sdk/roadmap'}
collapsed={collapsed}
projectId={projectId}
/>
<AdditionalModuleItem
href="/sdk/isms"
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
}
label="ISMS (ISO 27001)"
isActive={pathname === '/sdk/isms'}
collapsed={collapsed}
projectId={projectId}
/>
<AdditionalModuleItem
href="/sdk/audit-llm"
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
}
label="LLM Audit"
isActive={pathname === '/sdk/audit-llm'}
collapsed={collapsed}
projectId={projectId}
/>
<AdditionalModuleItem
href="/sdk/rbac"
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
}
label="RBAC Admin"
isActive={pathname === '/sdk/rbac'}
collapsed={collapsed}
projectId={projectId}
/>
<AdditionalModuleItem
href="/sdk/catalog-manager"
icon={
<svg className="w-5 h-5" 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 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
</svg>
}
label="Kataloge"
isActive={pathname === '/sdk/catalog-manager'}
collapsed={collapsed}
projectId={projectId}
/>
<AdditionalModuleItem
href="/sdk/wiki"
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
</svg>
}
label="Compliance Wiki"
isActive={pathname?.startsWith('/sdk/wiki')}
collapsed={collapsed}
projectId={projectId}
/>
<AdditionalModuleItem
href="/sdk/api-docs"
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
</svg>
}
label="API-Referenz"
isActive={pathname === '/sdk/api-docs'}
collapsed={collapsed}
projectId={projectId}
/>
<Link
href={withProject('/sdk/change-requests', projectId)}
className={`flex items-center gap-3 px-4 py-2.5 text-sm transition-colors ${
collapsed ? 'justify-center' : ''
} ${
pathname === '/sdk/change-requests'
? 'bg-purple-100 text-purple-900 font-medium'
: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'
}`}
title={collapsed ? `Änderungsanfragen (${pendingCRCount})` : undefined}
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
</svg>
{!collapsed && (
<span className="flex items-center gap-2">
Änderungsanfragen
{pendingCRCount > 0 && (
<span className="px-1.5 py-0.5 text-xs font-bold bg-red-500 text-white rounded-full min-w-[1.25rem] text-center">
{pendingCRCount}
</span>
)}
</span>
)}
{collapsed && pendingCRCount > 0 && (
<span className="absolute top-1 right-1 w-2 h-2 bg-red-500 rounded-full" />
)}
</Link>
<AdditionalModuleItem
href="https://macmini:3006"
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
</svg>
}
label="Developer Portal"
isActive={false}
collapsed={collapsed}
projectId={projectId}
/>
<AdditionalModuleItem
href="https://macmini:8011"
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
</svg>
}
label="SDK Dokumentation"
isActive={false}
collapsed={collapsed}
projectId={projectId}
/>
</div>
<SidebarModuleList
collapsed={collapsed}
projectId={projectId}
pendingCRCount={pendingCRCount}
/>
</nav>
{/* Footer */}
<div className={`${collapsed ? 'p-2' : 'p-4'} border-t border-gray-200 bg-gray-50`}>
{/* Collapse Toggle */}
<button
onClick={() => onCollapsedChange?.(!collapsed)}
className={`w-full flex items-center justify-center gap-2 ${collapsed ? 'p-2' : 'px-4 py-2'} text-sm text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors ${collapsed ? '' : 'mb-2'}`}
@@ -895,19 +213,13 @@ export function SDKSidebar({ collapsed = false, onCollapsedChange }: SDKSidebarP
{!collapsed && <span>Einklappen</span>}
</button>
{/* Export Button */}
{!collapsed && (
<button
onClick={() => {}}
className="w-full flex items-center justify-center gap-2 px-4 py-2 text-sm text-purple-600 hover:text-purple-700 hover:bg-purple-50 rounded-lg 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="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"
/>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
</svg>
<span>Exportieren</span>
</button>

View File

@@ -0,0 +1,40 @@
'use client'
// =============================================================================
// SIDEBAR ICON COMPONENTS
// Small SVG icons used in SDKSidebar sub-components.
// =============================================================================
export const CheckIcon = () => (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
)
export const LockIcon = () => (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
)
export const WarningIcon = () => (
<svg className="w-4 h-4" 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>
)
export const ChevronDownIcon = ({ className = '' }: { className?: string }) => (
<svg className={`w-4 h-4 ${className}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
)
export const CollapseIcon = ({ collapsed }: { collapsed: boolean }) => (
<svg
className={`w-5 h-5 transition-transform duration-300 ${collapsed ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 19l-7-7 7-7m8 14l-7-7 7-7" />
</svg>
)

View File

@@ -0,0 +1,108 @@
'use client'
// =============================================================================
// SIDEBAR ADDITIONAL MODULE LIST
// The "Zusatzmodule" and "Maschinenrecht / CE" sections rendered in the nav.
// =============================================================================
import React from 'react'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { AdditionalModuleItem, withProject } from './SidebarSubComponents'
interface SidebarModuleListProps {
collapsed: boolean
projectId?: string
pendingCRCount: number
}
export function SidebarModuleList({ collapsed, projectId, pendingCRCount }: SidebarModuleListProps) {
const pathname = usePathname()
return (
<>
{/* Maschinenrecht / CE */}
<div className="border-t border-gray-100 py-2">
{!collapsed && (
<div className="px-4 py-2 text-xs font-medium text-gray-400 uppercase tracking-wider">
Maschinenrecht / CE
</div>
)}
<AdditionalModuleItem
href="/sdk/iace"
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
</svg>
}
label="CE-Compliance (IACE)"
isActive={pathname?.startsWith('/sdk/iace') ?? false}
collapsed={collapsed}
projectId={projectId}
/>
</div>
{/* Additional Modules */}
<div className="border-t border-gray-100 py-2">
{!collapsed && (
<div className="px-4 py-2 text-xs font-medium text-gray-400 uppercase tracking-wider">
Zusatzmodule
</div>
)}
<AdditionalModuleItem href="/sdk/training" icon={<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" /></svg>} label="Schulung (Admin)" isActive={pathname === '/sdk/training'} collapsed={collapsed} projectId={projectId} />
<AdditionalModuleItem href="/sdk/training/learner" icon={<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" /></svg>} label="Schulung (Learner)" isActive={pathname === '/sdk/training/learner'} collapsed={collapsed} projectId={projectId} />
<AdditionalModuleItem href="/sdk/rag" icon={<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>} label="Legal RAG" isActive={pathname === '/sdk/rag'} collapsed={collapsed} projectId={projectId} />
<AdditionalModuleItem href="/sdk/quality" icon={<svg className="w-5 h-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>} label="AI Quality" isActive={pathname === '/sdk/quality'} collapsed={collapsed} projectId={projectId} />
<AdditionalModuleItem href="/sdk/security-backlog" icon={<svg className="w-5 h-5" 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>} label="Security Backlog" isActive={pathname === '/sdk/security-backlog'} collapsed={collapsed} projectId={projectId} />
<AdditionalModuleItem href="/sdk/compliance-hub" icon={<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" /></svg>} label="Compliance Hub" isActive={pathname === '/sdk/compliance-hub'} collapsed={collapsed} projectId={projectId} />
<AdditionalModuleItem href="/sdk/assertions" icon={<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" /></svg>} label="Assertions" isActive={pathname === '/sdk/assertions'} collapsed={collapsed} projectId={projectId} />
<AdditionalModuleItem href="/sdk/dsms" icon={<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" /></svg>} label="DSMS" isActive={pathname === '/sdk/dsms'} collapsed={collapsed} projectId={projectId} />
<AdditionalModuleItem href="/sdk/sdk-flow" icon={<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>} label="SDK Flow" isActive={pathname === '/sdk/sdk-flow'} collapsed={collapsed} projectId={projectId} />
<AdditionalModuleItem href="/sdk/architecture" icon={<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01" /></svg>} label="Architektur" isActive={pathname === '/sdk/architecture'} collapsed={collapsed} projectId={projectId} />
<AdditionalModuleItem href="/sdk/agents" icon={<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" /></svg>} label="Agenten" isActive={pathname?.startsWith('/sdk/agents') ?? false} collapsed={collapsed} projectId={projectId} />
<AdditionalModuleItem href="/sdk/workshop" icon={<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z" /></svg>} label="Workshop" isActive={pathname === '/sdk/workshop'} collapsed={collapsed} projectId={projectId} />
<AdditionalModuleItem href="/sdk/portfolio" icon={<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" /></svg>} label="Portfolio" isActive={pathname === '/sdk/portfolio'} collapsed={collapsed} projectId={projectId} />
<AdditionalModuleItem href="/sdk/roadmap" icon={<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" /></svg>} label="Roadmap" isActive={pathname === '/sdk/roadmap'} collapsed={collapsed} projectId={projectId} />
<AdditionalModuleItem href="/sdk/isms" icon={<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" /></svg>} label="ISMS (ISO 27001)" isActive={pathname === '/sdk/isms'} collapsed={collapsed} projectId={projectId} />
<AdditionalModuleItem href="/sdk/audit-llm" icon={<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" /></svg>} label="LLM Audit" isActive={pathname === '/sdk/audit-llm'} collapsed={collapsed} projectId={projectId} />
<AdditionalModuleItem href="/sdk/rbac" icon={<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" /></svg>} label="RBAC Admin" isActive={pathname === '/sdk/rbac'} collapsed={collapsed} projectId={projectId} />
<AdditionalModuleItem href="/sdk/catalog-manager" icon={<svg className="w-5 h-5" 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 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" /></svg>} label="Kataloge" isActive={pathname === '/sdk/catalog-manager'} collapsed={collapsed} projectId={projectId} />
<AdditionalModuleItem href="/sdk/wiki" icon={<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" /></svg>} label="Compliance Wiki" isActive={pathname?.startsWith('/sdk/wiki')} collapsed={collapsed} projectId={projectId} />
<AdditionalModuleItem href="/sdk/api-docs" icon={<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" /></svg>} label="API-Referenz" isActive={pathname === '/sdk/api-docs'} collapsed={collapsed} projectId={projectId} />
{/* Change Requests — needs badge so handled directly */}
<Link
href={withProject('/sdk/change-requests', projectId)}
className={`flex items-center gap-3 px-4 py-2.5 text-sm transition-colors ${
collapsed ? 'justify-center' : ''
} ${
pathname === '/sdk/change-requests'
? 'bg-purple-100 text-purple-900 font-medium'
: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'
}`}
title={collapsed ? `Änderungsanfragen (${pendingCRCount})` : undefined}
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
</svg>
{!collapsed && (
<span className="flex items-center gap-2">
Änderungsanfragen
{pendingCRCount > 0 && (
<span className="px-1.5 py-0.5 text-xs font-bold bg-red-500 text-white rounded-full min-w-[1.25rem] text-center">
{pendingCRCount}
</span>
)}
</span>
)}
{collapsed && pendingCRCount > 0 && (
<span className="absolute top-1 right-1 w-2 h-2 bg-red-500 rounded-full" />
)}
</Link>
<AdditionalModuleItem href="https://macmini:3006" icon={<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" /></svg>} label="Developer Portal" isActive={false} collapsed={collapsed} projectId={projectId} />
<AdditionalModuleItem href="https://macmini:8011" icon={<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" /></svg>} label="SDK Dokumentation" isActive={false} collapsed={collapsed} projectId={projectId} />
</div>
</>
)
}

View File

@@ -0,0 +1,295 @@
'use client'
// =============================================================================
// SIDEBAR SUB-COMPONENTS
// ProgressBar, PackageIndicator, StepItem, AdditionalModuleItem,
// CorpusStalenessInfo — all used internally by SDKSidebar.
// =============================================================================
import React from 'react'
import Link from 'next/link'
import type { SDKStep, SDKPackageId, RAGCorpusStatus } from '@/lib/sdk'
import { CheckIcon, LockIcon, WarningIcon, ChevronDownIcon } from './SidebarIcons'
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
export function withProject(url: string, projectId?: string): string {
if (!projectId) return url
const separator = url.includes('?') ? '&' : '?'
return `${url}${separator}project=${projectId}`
}
// ---------------------------------------------------------------------------
// ProgressBar
// ---------------------------------------------------------------------------
interface ProgressBarProps {
value: number
className?: string
}
export function ProgressBar({ value, className = '' }: ProgressBarProps) {
return (
<div className={`h-1 bg-gray-200 rounded-full overflow-hidden ${className}`}>
<div
className="h-full bg-purple-600 rounded-full transition-all duration-500"
style={{ width: `${Math.min(100, Math.max(0, value))}%` }}
/>
</div>
)
}
// ---------------------------------------------------------------------------
// PackageIndicator
// ---------------------------------------------------------------------------
interface PackageIndicatorProps {
packageId: SDKPackageId
order: number
name: string
icon: string
completion: number
isActive: boolean
isExpanded: boolean
isLocked: boolean
onToggle: () => void
collapsed: boolean
}
export function PackageIndicator({
order,
name,
icon,
completion,
isActive,
isExpanded,
isLocked,
onToggle,
collapsed,
}: PackageIndicatorProps) {
if (collapsed) {
return (
<button
onClick={onToggle}
className={`w-full flex items-center justify-center py-3 transition-colors ${
isActive
? 'bg-purple-50 border-l-4 border-purple-600'
: isLocked
? 'border-l-4 border-transparent opacity-50'
: 'hover:bg-gray-50 border-l-4 border-transparent'
}`}
title={`${order}. ${name} (${completion}%)`}
>
<div
className={`w-8 h-8 rounded-full flex items-center justify-center text-lg ${
isLocked
? 'bg-gray-200 text-gray-400'
: isActive
? 'bg-purple-600 text-white'
: completion === 100
? 'bg-green-500 text-white'
: 'bg-gray-200 text-gray-600'
}`}
>
{isLocked ? <LockIcon /> : completion === 100 ? <CheckIcon /> : icon}
</div>
</button>
)
}
return (
<button
onClick={onToggle}
disabled={isLocked}
className={`w-full flex items-center justify-between px-4 py-3 text-left transition-colors ${
isLocked
? 'opacity-50 cursor-not-allowed'
: isActive
? 'bg-purple-50 border-l-4 border-purple-600'
: 'hover:bg-gray-50 border-l-4 border-transparent'
}`}
>
<div className="flex items-center gap-3">
<div
className={`w-8 h-8 rounded-full flex items-center justify-center text-lg ${
isLocked
? 'bg-gray-200 text-gray-400'
: isActive
? 'bg-purple-600 text-white'
: completion === 100
? 'bg-green-500 text-white'
: 'bg-gray-200 text-gray-600'
}`}
>
{isLocked ? <LockIcon /> : completion === 100 ? <CheckIcon /> : icon}
</div>
<div>
<div className={`font-medium text-sm ${isActive ? 'text-purple-900' : isLocked ? 'text-gray-400' : 'text-gray-700'}`}>
{order}. {name}
</div>
<div className="text-xs text-gray-500">{completion}%</div>
</div>
</div>
{!isLocked && <ChevronDownIcon className={`transition-transform ${isExpanded ? 'rotate-180' : ''}`} />}
</button>
)
}
// ---------------------------------------------------------------------------
// StepItem
// ---------------------------------------------------------------------------
interface StepItemProps {
step: SDKStep
isActive: boolean
isCompleted: boolean
isLocked: boolean
checkpointStatus?: 'passed' | 'failed' | 'warning' | 'pending'
collapsed: boolean
projectId?: string
}
export function StepItem({ step, isActive, isCompleted, isLocked, checkpointStatus, collapsed, projectId }: StepItemProps) {
const content = (
<div
className={`flex items-center gap-3 px-4 py-2.5 text-sm transition-colors ${
collapsed ? 'justify-center' : ''
} ${
isActive
? 'bg-purple-100 text-purple-900 font-medium'
: isLocked
? 'text-gray-400 cursor-not-allowed'
: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'
}`}
title={collapsed ? step.name : undefined}
>
<div className="flex-shrink-0">
{isCompleted ? (
<div className="w-5 h-5 rounded-full bg-green-500 text-white flex items-center justify-center">
<CheckIcon />
</div>
) : isLocked ? (
<div className="w-5 h-5 rounded-full bg-gray-200 text-gray-400 flex items-center justify-center">
<LockIcon />
</div>
) : isActive ? (
<div className="w-5 h-5 rounded-full bg-purple-600 flex items-center justify-center">
<div className="w-2 h-2 rounded-full bg-white" />
</div>
) : (
<div className="w-5 h-5 rounded-full border-2 border-gray-300" />
)}
</div>
{!collapsed && <span className="flex-1 truncate">{step.nameShort}</span>}
{!collapsed && checkpointStatus && checkpointStatus !== 'pending' && (
<div className="flex-shrink-0">
{checkpointStatus === 'passed' ? (
<div className="w-4 h-4 rounded-full bg-green-100 text-green-600 flex items-center justify-center">
<CheckIcon />
</div>
) : checkpointStatus === 'failed' ? (
<div className="w-4 h-4 rounded-full bg-red-100 text-red-600 flex items-center justify-center">
<span className="text-xs font-bold">!</span>
</div>
) : (
<div className="w-4 h-4 rounded-full bg-yellow-100 text-yellow-600 flex items-center justify-center">
<WarningIcon />
</div>
)}
</div>
)}
</div>
)
if (isLocked) return content
return (
<Link href={withProject(step.url, projectId)} className="block">
{content}
</Link>
)
}
// ---------------------------------------------------------------------------
// AdditionalModuleItem
// ---------------------------------------------------------------------------
interface AdditionalModuleItemProps {
href: string
icon: React.ReactNode
label: string
isActive: boolean
collapsed: boolean
projectId?: string
}
export function AdditionalModuleItem({ href, icon, label, isActive, collapsed, projectId }: AdditionalModuleItemProps) {
const isExternal = href.startsWith('http')
const className = `flex items-center gap-3 px-4 py-2.5 text-sm transition-colors ${
collapsed ? 'justify-center' : ''
} ${
isActive
? 'bg-purple-100 text-purple-900 font-medium'
: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'
}`
if (isExternal) {
return (
<a href={href} target="_blank" rel="noopener noreferrer" className={className} title={collapsed ? label : undefined}>
{icon}
{!collapsed && (
<span className="flex items-center gap-1">
{label}
<svg className="w-3 h-3 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</span>
)}
</a>
)
}
return (
<Link href={withProject(href, projectId)} className={className} title={collapsed ? label : undefined}>
{icon}
{!collapsed && <span>{label}</span>}
</Link>
)
}
// ---------------------------------------------------------------------------
// CorpusStalenessInfo
// ---------------------------------------------------------------------------
export function CorpusStalenessInfo({ ragCorpusStatus }: { ragCorpusStatus: RAGCorpusStatus }) {
const collections = ragCorpusStatus.collections
const collectionNames = Object.keys(collections)
if (collectionNames.length === 0) return null
const lastUpdated = collectionNames.reduce((latest, name) => {
const updated = new Date(collections[name].last_updated)
return updated > latest ? updated : latest
}, new Date(0))
const daysSinceUpdate = Math.floor((Date.now() - lastUpdated.getTime()) / (1000 * 60 * 60 * 24))
const totalChunks = collectionNames.reduce((sum, name) => sum + collections[name].chunks_count, 0)
return (
<div className="px-4 py-2 border-b border-gray-100">
<div className="flex items-center gap-2 text-xs">
<div className={`w-2 h-2 rounded-full flex-shrink-0 ${daysSinceUpdate > 30 ? 'bg-amber-400' : 'bg-green-400'}`} />
<span className="text-gray-500 truncate">
RAG Corpus: {totalChunks} Chunks
</span>
</div>
{daysSinceUpdate > 30 && (
<div className="mt-1 text-xs text-amber-600 bg-amber-50 rounded px-2 py-1">
Corpus {daysSinceUpdate}d alt Re-Evaluation empfohlen
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,16 @@
// =============================================================================
// STEP EXPLANATION PRESETS — merged index
// Combines Part1 (company-profile … einwilligungen) and
// Part2 (dsr … use-case-workshop) for backward-compatible re-export.
// =============================================================================
import type { StepTip } from './StepHeader'
import { STEP_EXPLANATIONS_PART1 } from './StepExplanationsPart1'
import { STEP_EXPLANATIONS_PART2 } from './StepExplanationsPart2'
type ExplanationEntry = { title: string; description: string; explanation: string; tips: StepTip[] }
export const STEP_EXPLANATIONS: Record<string, ExplanationEntry> = {
...STEP_EXPLANATIONS_PART1,
...STEP_EXPLANATIONS_PART2,
}

View File

@@ -0,0 +1,197 @@
// =============================================================================
// STEP EXPLANATION PRESETS — Part 1 (company-profile … einwilligungen)
// =============================================================================
import type { StepTip } from './StepHeader'
type ExplanationEntry = { title: string; description: string; explanation: string; tips: StepTip[] }
export const STEP_EXPLANATIONS_PART1: Record<string, ExplanationEntry> = {
'company-profile': {
title: 'Unternehmensprofil',
description: 'Erfassen Sie Ihr Geschäftsmodell und Ihre Zielmärkte',
explanation: 'Im Unternehmensprofil erfassen wir grundlegende Informationen zu Ihrem Unternehmen: Geschäftsmodell (B2B/B2C), Angebote, Firmengröße und Zielmärkte. Diese Informationen helfen uns, die für Sie relevanten Regulierungen zu identifizieren und ehrlich zu kommunizieren, wo unsere Grenzen liegen.',
tips: [
{ icon: 'lightbulb' as const, title: 'Ehrliche Einschätzung', description: 'Wir zeigen Ihnen transparent, welche Regulierungen wir abdecken und wann Sie einen Anwalt hinzuziehen sollten.' },
{ icon: 'info' as const, title: 'Zielmärkte', description: 'Je nach Zielmarkt (Deutschland, DACH, EU, weltweit) gelten unterschiedliche Datenschutzgesetze.' },
],
},
'compliance-scope': {
title: 'Compliance Scope',
description: 'Umfang und Tiefe Ihrer Compliance-Dokumentation bestimmen',
explanation: 'Die Compliance Scope Engine bestimmt deterministisch, welche Dokumente Sie in welcher Tiefe benoetigen. Basierend auf 35 Fragen in 6 Bloecken werden Risiko-, Komplexitaets- und Assurance-Scores berechnet, die in ein 4-Level-Modell (L1 Lean bis L4 Zertifizierungsbereit) muenden.',
tips: [
{ icon: 'lightbulb' as const, title: 'Deterministisch', description: 'Alle Entscheidungen sind nachvollziehbar — keine KI, keine Black Box. Jede Einstufung wird mit Rechtsgrundlage und Audit-Trail begruendet.' },
{ icon: 'info' as const, title: '4-Level-Modell', description: 'L1 (Lean Startup) bis L4 (Zertifizierungsbereit). Hard Triggers (Art. 9, Minderjaehrige, Zertifizierungsziele) heben das Level automatisch an.' },
{ icon: 'warning' as const, title: 'Hard Triggers', description: '50 deterministische Regeln pruefen besondere Kategorien (Art. 9), Minderjaehrige, KI-Einsatz, Drittlandtransfers und Zertifizierungsziele.' },
],
},
'use-case-assessment': {
title: 'Anwendungsfall-Erfassung',
description: 'Erfassen Sie Ihre KI-Anwendungsfälle systematisch',
explanation: 'In der Anwendungsfall-Erfassung dokumentieren Sie Ihre KI-Anwendungsfälle in 5 Schritten: Grunddaten, Datenkategorien, Risikobewertung, Stakeholder und Compliance-Anforderungen. Dies bildet die Basis für alle weiteren Compliance-Maßnahmen.',
tips: [
{ icon: 'lightbulb' as const, title: 'Tipp: Vollständigkeit', description: 'Je detaillierter Sie den Anwendungsfall beschreiben, desto besser kann das System passende Compliance-Anforderungen ableiten.' },
{ icon: 'info' as const, title: 'Mehrere Anwendungsfälle', description: 'Sie können mehrere Anwendungsfälle erfassen. Jeder wird separat bewertet und durchläuft den Compliance-Prozess.' },
],
},
'screening': {
title: 'System Screening',
description: 'Analysieren Sie Ihre Systemlandschaft auf Schwachstellen',
explanation: 'Das System Screening generiert eine Software Bill of Materials (SBOM) und fuehrt einen Security-Scan durch. So erkennen Sie Schwachstellen in Ihren Abhaengigkeiten fruehzeitig.',
tips: [
{ icon: 'warning' as const, title: 'Kritische Schwachstellen', description: 'CVEs mit CVSS >= 7.0 sollten priorisiert behandelt werden. Diese werden automatisch in den Security Backlog uebernommen.' },
{ icon: 'info' as const, title: 'SBOM-Format', description: 'Die SBOM wird im CycloneDX-Format generiert und kann fuer Audits exportiert werden.' },
],
},
'modules': {
title: 'Compliance Module',
description: 'Waehlen Sie die relevanten Regulierungen fuer Ihr Unternehmen',
explanation: 'Compliance-Module sind vordefinierte Regelwerke (z.B. DSGVO, AI Act, ISO 27001). Durch die Aktivierung eines Moduls werden automatisch die zugehoerigen Anforderungen und Kontrollen geladen.',
tips: [
{ icon: 'lightbulb' as const, title: 'Modul-Auswahl', description: 'Aktivieren Sie nur Module, die fuer Ihr Unternehmen relevant sind. Weniger ist oft mehr - fokussieren Sie sich auf die wichtigsten Regulierungen.' },
{ icon: 'info' as const, title: 'Abhaengigkeiten', description: 'Manche Module haben Ueberschneidungen. Das System erkennt dies automatisch und vermeidet doppelte Anforderungen.' },
],
},
'requirements': {
title: 'Anforderungen',
description: 'Pruefen und verwalten Sie die Compliance-Anforderungen',
explanation: 'Anforderungen sind konkrete Vorgaben aus den aktivierten Modulen. Jede Anforderung verweist auf einen Gesetzesartikel und muss durch Kontrollen abgedeckt werden. Vollstaendige CRUD-Operationen mit Backend-Persistenz.',
tips: [
{ icon: 'warning' as const, title: 'Kritische Anforderungen', description: 'Anforderungen mit Kritikalitaet "HOCH" sollten priorisiert werden, da Verstoesse zu hohen Bussgeldern fuehren koennen.' },
{ icon: 'success' as const, title: 'Status-Workflow', description: 'Anforderungen durchlaufen: Nicht begonnen → In Bearbeitung → Implementiert → Verifiziert. Bei Backend-Fehler erfolgt automatischer Rollback.' },
{ icon: 'lightbulb' as const, title: 'CRUD-Operationen', description: 'Anforderungen koennen erstellt, bearbeitet und geloescht werden. Alle Aenderungen werden sofort im Backend persistiert.' },
],
},
'controls': {
title: 'Kontrollen',
description: 'Definieren Sie technische und organisatorische Massnahmen',
explanation: 'Kontrollen (auch TOMs genannt) sind konkrete Massnahmen zur Erfuellung der Anforderungen. Sie koennen praeventiv, detektiv oder korrektiv sein. Evidence-Linking zeigt verknuepfte Nachweise mit Gueltigkeits-Badge.',
tips: [
{ icon: 'lightbulb' as const, title: 'Wirksamkeit', description: 'Bewerten Sie die Wirksamkeit jeder Kontrolle. Eine hohe Wirksamkeit (>80%) reduziert das Restrisiko erheblich.' },
{ icon: 'info' as const, title: 'Verantwortlichkeiten', description: 'Weisen Sie jeder Kontrolle einen Verantwortlichen zu. Dies ist fuer Audits wichtig.' },
{ icon: 'success' as const, title: 'Evidence-Linking', description: 'Verknuepfen Sie Nachweise direkt mit Controls. Gueltige, abgelaufene und ausstehende Nachweise werden mit Badges angezeigt.' },
],
},
'evidence': {
title: 'Nachweise',
description: 'Dokumentieren Sie die Umsetzung mit Belegen',
explanation: 'Nachweise sind Dokumente, Screenshots oder Berichte, die belegen, dass Kontrollen implementiert sind. Server-seitige Pagination fuer grosse Nachweis-Sammlungen.',
tips: [
{ icon: 'warning' as const, title: 'Gueltigkeit', description: 'Achten Sie auf das Ablaufdatum von Nachweisen. Abgelaufene Zertifikate oder Berichte muessen erneuert werden. Status: valid, expired, pending, failed.' },
{ icon: 'success' as const, title: 'Verknuepfung', description: 'Verknuepfen Sie Nachweise direkt mit den zugehoerigen Kontrollen fuer eine lueckenlose Dokumentation.' },
{ icon: 'info' as const, title: 'Pagination', description: 'Bei vielen Nachweisen wird automatisch paginiert. Nutzen Sie die Seitennavigation am Ende der Liste.' },
],
},
'audit-checklist': {
title: 'Audit-Checkliste',
description: 'Systematische Pruefung der Compliance-Konformitaet',
explanation: 'Die Audit-Checkliste wird automatisch aus den Anforderungen generiert. Session-Management mit Sign-Off-Workflow und PDF-Export.',
tips: [
{ icon: 'lightbulb' as const, title: 'Regelmaessige Pruefung', description: 'Fuehren Sie die Checkliste mindestens jaehrlich durch, um Compliance-Luecken fruehzeitig zu erkennen.' },
{ icon: 'info' as const, title: 'Sign-Off & PDF', description: 'Zeichnen Sie Pruefpunkte mit digitalem Hash (SHA-256) ab. Exportieren Sie den Report als PDF in Deutsch oder Englisch.' },
{ icon: 'success' as const, title: 'Session-History', description: 'Vergangene Audit-Sitzungen werden mit Status-Badges angezeigt: Draft, In Progress, Completed, Archived.' },
],
},
'risks': {
title: 'Risiko-Matrix',
description: 'Bewerten und priorisieren Sie Ihre Compliance-Risiken',
explanation: 'Die 5x5 Risiko-Matrix visualisiert Ihre Risiken nach Wahrscheinlichkeit und Auswirkung. Inherent Risk vs. Residual Risk mit visuellem Vergleich.',
tips: [
{ icon: 'warning' as const, title: 'Kritische Risiken', description: 'Risiken mit Score >= 20 sind CRITICAL (rot), >= 12 HIGH (orange), >= 6 MEDIUM (gelb), < 6 LOW (gruen).' },
{ icon: 'success' as const, title: 'Mitigation', description: 'Verknuepfen Sie Controls als Mitigationsmassnahmen. Der Residual-Risk wird automatisch anhand verknuepfter Controls berechnet.' },
{ icon: 'info' as const, title: 'Status-Workflow', description: 'Risiken durchlaufen: Identifiziert → Bewertet → Mitigiert → Akzeptiert → Geschlossen.' },
],
},
'ai-act': {
title: 'AI Act Klassifizierung',
description: 'Registrieren und klassifizieren Sie Ihre KI-Systeme',
explanation: 'Der EU AI Act klassifiziert KI-Systeme in Risikostufen: Minimal, Begrenzt, Hoch und Verboten. KI-Systeme werden im Backend persistent gespeichert und koennen automatisch bewertet werden.',
tips: [
{ icon: 'warning' as const, title: 'Hochrisiko-Systeme', description: 'Hochrisiko-KI erfordert 8 Pflichten: Risikomanagement, Daten-Governance, Dokumentation, Transparenz, menschliche Aufsicht, Genauigkeit, Robustheit, Cybersicherheit.' },
{ icon: 'lightbulb' as const, title: 'Automatische Bewertung', description: 'Nutzen Sie die Assess-Funktion: Sie analysiert Zweck und Sektor und leitet die Risikokategorie + Pflichten automatisch ab.' },
{ icon: 'info' as const, title: 'CRUD-Operationen', description: 'KI-Systeme koennen registriert, bearbeitet, bewertet und geloescht werden. Alle Daten werden backend-persistent gespeichert.' },
],
},
'dsfa': {
title: 'Datenschutz-Folgenabschaetzung',
description: 'Erstellen Sie eine DSFA fuer Hochrisiko-Verarbeitungen',
explanation: 'Eine DSFA (Art. 35 DSGVO) ist erforderlich, wenn eine Verarbeitung voraussichtlich hohe Risiken fuer Betroffene birgt. Das Tool fuehrt Sie durch alle erforderlichen Abschnitte.',
tips: [
{ icon: 'warning' as const, title: 'Pflicht', description: 'Eine DSFA ist Pflicht bei: Profiling mit rechtlicher Wirkung, umfangreicher Verarbeitung besonderer Datenkategorien, systematischer Ueberwachung.' },
{ icon: 'lightbulb' as const, title: 'Konsultation', description: 'Bei hohem Restrisiko muss die Aufsichtsbehoerde konsultiert werden (Art. 36 DSGVO).' },
],
},
'tom': {
title: 'Technische und Organisatorische Massnahmen',
description: 'TOMs nach Art. 32 DSGVO mit Vendor-Controls-Querverweis',
explanation: 'TOMs sind konkrete Sicherheitsmassnahmen zum Schutz personenbezogener Daten. Das Dashboard zeigt den Status aller aus dem TOM Generator abgeleiteten Massnahmen mit SDM-Mapping und Gap-Analyse. Im Uebersicht-Tab werden zusaetzlich Vendor-TOM-Controls (VND-TOM-01 bis VND-TOM-06) aus dem Vendor-Compliance-Modul als Querverweis angezeigt.',
tips: [
{ icon: 'warning' as const, title: 'Nachweispflicht', description: 'TOMs muessen nachweisbar real sein. Verknuepfen Sie Evidence-Dokumente (Policies, Zertifikate, Screenshots) mit jeder Massnahme, um die Rechenschaftspflicht (Art. 5 Abs. 2 DSGVO) zu erfuellen.' },
{ icon: 'info' as const, title: 'Generator nutzen', description: 'Der 6-Schritt-Wizard leitet TOMs systematisch aus Ihrem Risikoprofil ab. Starten Sie dort, um eine vollstaendige Baseline zu erhalten.' },
{ icon: 'info' as const, title: 'SDM-Mapping', description: 'Kontrollen werden den 7 SDM-Gewaehrleistungszielen zugeordnet: Verfuegbarkeit, Integritaet, Vertraulichkeit, Nichtverkettung, Intervenierbarkeit, Transparenz, Datenminimierung.' },
{ icon: 'success' as const, title: 'Vendor-Controls', description: 'Im Uebersicht-Tab werden Vendor-TOM-Controls (VND-TOM-01 bis 06) als Read-Only-Querverweis angezeigt: Verschluesselung, Zugriffskontrolle, Verfuegbarkeit und Ueberpruefungsverfahren Ihrer Auftragsverarbeiter.' },
],
},
'vvt': {
title: 'Verarbeitungsverzeichnis',
description: 'Verarbeitungsverzeichnis nach Art. 30 DSGVO mit integriertem Processor-Tab',
explanation: 'Das Verarbeitungsverzeichnis (VVT) dokumentiert alle Verarbeitungstaetigkeiten mit personenbezogenen Daten. Der integrierte Generator-Fragebogen befuellt 70-90% der Pflichtfelder automatisch. Der Tab "Auftragsverarbeiter (Abs. 2)" liest Vendors mit role=PROCESSOR/SUB_PROCESSOR direkt aus der Vendor-Compliance-API — keine doppelte Datenhaltung.',
tips: [
{ icon: 'warning' as const, title: 'Pflicht fuer alle', description: 'Die Ausnahme fuer Unternehmen <250 Mitarbeiter greift nur bei gelegentlicher, risikoarmer Verarbeitung ohne besondere Kategorien (Art. 30 Abs. 5).' },
{ icon: 'info' as const, title: 'Zweck-zuerst', description: 'Definieren Sie Verarbeitungen nach Geschaeftszweck, nicht nach Tool. Ein Tool kann mehrere Verarbeitungen abdecken, eine Verarbeitung kann mehrere Tools nutzen.' },
{ icon: 'info' as const, title: 'Kein oeffentliches Dokument', description: 'Das VVT ist ein internes Dokument. Es muss der Aufsichtsbehoerde nur auf Verlangen vorgelegt werden (Art. 30 Abs. 4).' },
{ icon: 'success' as const, title: 'Processor-Tab (Art. 30 Abs. 2)', description: 'Auftragsverarbeiter werden direkt aus dem Vendor Register gelesen (Read-Only). Neue Vendors werden im Vendor-Compliance-Modul angelegt und erscheinen hier automatisch. PDF-Druck fuer Art. 30 Abs. 2 Dokument.' },
],
},
'cookie-banner': {
title: 'Cookie Banner',
description: 'Konfigurieren Sie einen DSGVO-konformen Cookie Banner mit persistenter DB-Speicherung',
explanation: 'Der Cookie Banner Generator erstellt einen rechtssicheren Banner mit Opt-In fuer nicht-essentielle Cookies. Alle Einstellungen — einschliesslich Ueberschrift, Beschreibung und Datenschutz-Link — werden in der Datenbank gespeichert und bleiben auch nach einem Neustart erhalten. Der generierte Embed-Code wird direkt aus der gespeicherten Konfiguration erzeugt.',
tips: [
{ icon: 'warning' as const, title: 'Opt-In Pflicht', description: 'Fuer Marketing- und Analytics-Cookies ist eine aktive Einwilligung erforderlich. Vorangekreuzte Checkboxen sind nicht erlaubt.' },
{ icon: 'lightbulb' as const, title: 'Design und Texte', description: 'Passen Sie Ueberschrift, Beschreibung und Farben an Ihr Corporate Design an. Aenderungen werden in der Vorschau sofort sichtbar.' },
{ icon: 'info' as const, title: 'Embed-Code', description: 'Der Code exportiert einen vollstaendigen HTML+CSS+JS-Block aus Ihrer gespeicherten Konfiguration — einfach vor dem schliessenden </body>-Tag einbinden.' },
],
},
'obligations': {
title: 'Pflichtenuebersicht',
description: 'Regulatorische Pflichten mit 12 Compliance-Checks und Vendor-Verknuepfung',
explanation: 'Die Pflichtenuebersicht aggregiert alle Anforderungen aus DSGVO, AI Act, NIS2 und weiteren Regulierungen. 12 automatische Compliance-Checks pruefen Vollstaendigkeit, Fristen, Nachweise und Vendor-Verknuepfungen. Art.-28-Pflichten koennen mit Auftragsverarbeitern aus dem Vendor Register verknuepft werden. Das Pflichtenregister-Dokument (11 Sektionen) kann als auditfaehiges PDF gedruckt werden.',
tips: [
{ icon: 'info' as const, title: 'Filterung', description: 'Filtern Sie nach Regulierung, Prioritaet oder Status, um die relevanten Pflichten schnell zu finden.' },
{ icon: 'warning' as const, title: 'Compliance-Checks', description: '12 automatische Checks: Fehlende Verantwortliche, ueberfaellige Fristen, fehlende Nachweise, keine Rechtsreferenz, stagnierende Regulierungen, nicht gestartete High-Priority-Pflichten, fehlende Vendor-Verknuepfung (Art. 28) u.v.m.' },
{ icon: 'success' as const, title: 'Vendor-Verknuepfung', description: 'Art.-28-Pflichten (Auftragsverarbeitung) koennen direkt mit Vendors aus dem Vendor Register verknuepft werden. Check #12 (MISSING_VENDOR_LINK) warnt bei fehlender Verknuepfung.' },
{ icon: 'lightbulb' as const, title: 'Pflichtenregister-Dokument', description: 'Generieren Sie ein auditfaehiges Pflichtenregister mit 11 Sektionen: Ziel, Geltungsbereich, Methodik, Regulatorische Grundlagen, Pflichtenuebersicht, Details, Verantwortlichkeiten, Fristen, Nachweisverzeichnis, Compliance-Status und Aenderungshistorie.' },
],
},
'loeschfristen': {
title: 'Loeschfristen',
description: 'Aufbewahrungsrichtlinien mit VVT-Verknuepfung und Vendor-Zuordnung',
explanation: 'Loeschfristen legen fest, wie lange personenbezogene Daten gespeichert werden duerfen. Die 3-Stufen-Logik (Zweckende, Aufbewahrungspflicht, Legal Hold) stellt sicher, dass alle gesetzlichen Anforderungen beruecksichtigt werden. Policies koennen mit VVT-Verarbeitungstaetigkeiten und Auftragsverarbeitern aus dem Vendor Register verknuepft werden.',
tips: [
{ icon: 'warning' as const, title: '3-Stufen-Logik', description: 'Jede Loeschfrist folgt einer 3-Stufen-Logik: 1. Zweckende (Daten werden nach Zweckwegfall geloescht), 2. Aufbewahrungspflicht (gesetzliche Fristen verhindern Loeschung), 3. Legal Hold (laufende Verfahren blockieren Loeschung).' },
{ icon: 'info' as const, title: 'Deutsche Rechtsgrundlagen', description: 'Der Generator kennt die wichtigsten Aufbewahrungstreiber: AO (10 J. Steuer), HGB (10/6 J. Handel), UStG (10 J. Rechnungen), BGB (3 J. Verjaehrung), ArbZG (2 J. Zeiterfassung), AGG (6 Mon. Bewerbungen).' },
{ icon: 'info' as const, title: 'Backup-Behandlung', description: 'Auch Backups muessen ins Loeschkonzept einbezogen werden. Daten koennen nach primaerer Loeschung noch in Backup-Systemen existieren.' },
{ icon: 'success' as const, title: 'Vendor-Verknuepfung', description: 'Loeschfrist-Policies koennen mit Auftragsverarbeitern verknuepft werden. So ist dokumentiert, welche Vendors Loeschpflichten fuer bestimmte Datenkategorien haben.' },
],
},
'consent': {
title: 'Rechtliche Vorlagen',
description: 'Generieren Sie AGB, Datenschutzerklaerung und Nutzungsbedingungen',
explanation: 'Die rechtlichen Vorlagen werden basierend auf Ihren Verarbeitungstaetigkeiten und Use Cases generiert. Sie sind auf Ihre spezifische Situation zugeschnitten.',
tips: [
{ icon: 'info' as const, title: 'Anpassung', description: 'Die generierten Vorlagen koennen und sollten an Ihre spezifischen Anforderungen angepasst werden.' },
{ icon: 'warning' as const, title: 'Rechtspruefung', description: 'Lassen Sie die finalen Dokumente von einem Rechtsanwalt pruefen, bevor Sie sie veroeffentlichen.' },
],
},
'einwilligungen': {
title: 'Einwilligungen',
description: 'Verwalten Sie Consent-Tracking und Einwilligungsnachweise',
explanation: 'Hier konfigurieren Sie, wie Einwilligungen erfasst, gespeichert und nachgewiesen werden. Dies ist essentiell fuer den Nachweis der Rechtmaessigkeit.',
tips: [
{ icon: 'success' as const, title: 'Nachweis', description: 'Speichern Sie fuer jede Einwilligung: Zeitpunkt, Version des Textes, Art der Einwilligung.' },
{ icon: 'info' as const, title: 'Widerruf', description: 'Stellen Sie sicher, dass Nutzer ihre Einwilligung jederzeit widerrufen koennen.' },
],
},
}

View File

@@ -0,0 +1,208 @@
// =============================================================================
// STEP EXPLANATION PRESETS — Part 2 (dsr … use-case-workshop)
// =============================================================================
import type { StepTip } from './StepHeader'
type ExplanationEntry = { title: string; description: string; explanation: string; tips: StepTip[] }
export const STEP_EXPLANATIONS_PART2: Record<string, ExplanationEntry> = {
'dsr': {
title: 'DSR Portal',
description: 'Richten Sie ein Portal fuer Betroffenenrechte ein',
explanation: 'Das DSR (Data Subject Rights) Portal ermoeglicht Betroffenen, ihre Rechte nach DSGVO auszuueben: Auskunft, Loeschung, Berichtigung, Datenportabilitaet.',
tips: [
{ icon: 'warning' as const, title: 'Fristen', description: 'Anfragen muessen innerhalb von 30 Tagen beantwortet werden. Richten Sie Workflows ein, um dies sicherzustellen.' },
{ icon: 'lightbulb' as const, title: 'Identitaetspruefung', description: 'Implementieren Sie eine sichere Identitaetspruefung, bevor Sie Daten herausgeben.' },
],
},
'escalations': {
title: 'Eskalations-Workflows',
description: 'Definieren Sie Management-Workflows fuer Compliance-Vorfaelle',
explanation: 'Eskalations-Workflows legen fest, wie auf Compliance-Vorfaelle reagiert wird: Wer wird informiert, welche Massnahmen werden ergriffen, wie wird dokumentiert.',
tips: [
{ icon: 'info' as const, title: 'Datenpannen', description: 'Bei Datenpannen muss die Aufsichtsbehoerde innerhalb von 72 Stunden informiert werden.' },
{ icon: 'success' as const, title: 'Verantwortlichkeiten', description: 'Definieren Sie klare Verantwortlichkeiten fuer jeden Schritt im Eskalationsprozess.' },
],
},
'vendor-compliance': {
title: 'Vendor Compliance',
description: 'Auftragsverarbeiter-Management mit Cross-Modul-Integration',
explanation: 'Vendor Compliance verwaltet alle Auftragsverarbeiter (Art. 28 DSGVO) und Drittanbieter. Fuer jeden Vendor werden AVVs, Drittlandtransfers, TOMs und Subunternehmer geprueft. Das Modul ist zentral mit vier weiteren Modulen integriert: VVT-Processor-Tab liest Vendors direkt aus der API, Obligations und Loeschfristen verknuepfen Vendors ueber linked_vendor_ids, TOM zeigt Vendor-Controls als Querverweis.',
tips: [
{ icon: 'warning' as const, title: 'Art. 28 DSGVO', description: 'Jede Auftragsverarbeitung erfordert einen schriftlichen Vertrag (AVV). Pruefen Sie: Weisungsgebundenheit, TOMs, Subunternehmer-Genehmigung, Loeschpflicht und Audit-Recht.' },
{ icon: 'info' as const, title: 'Cross-Modul-Integration', description: 'Vendors erscheinen automatisch im VVT-Processor-Tab, koennen in Obligations und Loeschfristen verknuepft werden, und ihre TOM-Controls werden im TOM-Modul als Querverweis angezeigt.' },
{ icon: 'lightbulb' as const, title: 'Drittlandtransfer', description: 'Bei Datenverarbeitung ausserhalb der EU/EWR sind Standardvertragsklauseln (SCCs) oder andere Garantien nach Art. 44-49 DSGVO erforderlich.' },
{ icon: 'success' as const, title: 'Controls Library', description: '6 TOM-Domain Controls (VND-TOM-01 bis VND-TOM-06) pruefen Verschluesselung, Zugriffskontrolle, Verfuegbarkeit und Ueberpruefungsverfahren bei Ihren Auftragsverarbeitern.' },
],
},
'document-generator': {
title: 'Dokumentengenerator',
description: 'Generieren Sie rechtliche Dokumente aus lizenzkonformen Vorlagen',
explanation: 'Der Dokumentengenerator nutzt frei lizenzierte Textbausteine (CC0, MIT, CC BY 4.0) um Datenschutzerklaerungen, AGB, Cookie-Banner und andere rechtliche Dokumente zu erstellen. Die Quellen werden mit korrekter Lizenz-Compliance und Attribution gehandhabt.',
tips: [
{ icon: 'lightbulb' as const, title: 'Lizenzfreie Vorlagen', description: 'Alle verwendeten Textbausteine stammen aus lizenzierten Quellen (CC0, MIT, CC BY 4.0). Die Attribution wird automatisch hinzugefuegt.' },
{ icon: 'info' as const, title: 'Platzhalter', description: 'Fuellen Sie die Platzhalter (z.B. [FIRMENNAME], [ADRESSE]) mit Ihren Unternehmensdaten aus.' },
{ icon: 'warning' as const, title: 'Rechtspruefung', description: 'Lassen Sie generierte Dokumente vor der Veroeffentlichung von einem Rechtsanwalt pruefen.' },
],
},
'source-policy': {
title: 'Source Policy',
description: 'Verwalten Sie Ihre Datenquellen-Governance',
explanation: 'Die Source Policy definiert, welche externen Datenquellen fuer Ihre Anwendung zugelassen sind. Sie umfasst eine Whitelist, Operationsmatrix (Lookup, RAG, Training, Export), PII-Regeln und ein Audit-Trail.',
tips: [
{ icon: 'warning' as const, title: 'Lizenzierung', description: 'Pruefen Sie die Lizenzen aller Datenquellen (DL-DE-BY, CC-BY, CC0). Nicht-lizenzierte Quellen koennen rechtliche Risiken bergen.' },
{ icon: 'info' as const, title: 'PII-Regeln', description: 'Definieren Sie klare Regeln fuer den Umgang mit personenbezogenen Daten in externen Quellen.' },
],
},
'audit-report': {
title: 'Audit Report',
description: 'Erstellen und verwalten Sie Audit-Sitzungen',
explanation: 'Im Audit Report erstellen Sie formelle Audit-Sitzungen. Uebersicht mit Status-Badges, Detail-Seite pro Sitzung mit Fortschrittsbalken und interaktiven Checklist-Items.',
tips: [
{ icon: 'lightbulb' as const, title: 'Regelmaessigkeit', description: 'Fuehren Sie mindestens jaehrlich ein formelles Audit durch. Dokumentieren Sie Abweichungen und Massnahmenplaene.' },
{ icon: 'success' as const, title: 'Detail-Ansicht', description: 'Klicken Sie auf eine Sitzung fuer die Detail-Seite: Metadaten, Fortschrittsbalken, Checklist-Items mit Sign-Off und Notizen.' },
{ icon: 'info' as const, title: 'PDF-Export', description: 'Generieren Sie PDF-Reports in Deutsch oder Englisch fuer externe Pruefer und Aufsichtsbehoerden.' },
],
},
'workflow': {
title: 'Document Workflow',
description: 'Freigabe-Workflow mit Split-View-Editor und DB-persistenter Versionierung',
explanation: 'Der Document Workflow bietet einen Split-View-Editor: links die veroffentlichte Version, rechts der aktuelle Entwurf. Dokumente durchlaufen den Status Draft → Review → Approved → Published. Alle Versionen werden in der Datenbank gespeichert. Word-Dokumente koennen direkt als neue Version importiert werden.',
tips: [
{ icon: 'warning' as const, title: 'Vier-Augen-Prinzip', description: 'Rechtliche Dokumente sollten immer von mindestens einer weiteren Person geprueft werden, bevor sie veroeffentlicht werden.' },
{ icon: 'info' as const, title: 'Versionierung', description: 'Jede Aenderung wird als neue Version gespeichert. Veroeffentlichte Versionen sind unveraenderlich — Aenderungen erzeugen stets eine neue Version.' },
{ icon: 'lightbulb' as const, title: 'DOCX-Import', description: 'Bestehende Word-Dokumente koennen direkt hochgeladen und als Basis fuer neue Versionen verwendet werden.' },
],
},
'consent-management': {
title: 'Consent Verwaltung',
description: 'Verwalten Sie Consent-Dokumente, Versionen und DSGVO-Prozesse',
explanation: 'Die Consent Verwaltung umfasst das Lifecycle-Management Ihrer rechtlichen Dokumente (AGB, Datenschutz, Cookie-Richtlinien), die Verwaltung von E-Mail-Templates (16 Lifecycle-E-Mails) und die Steuerung der DSGVO-Prozesse (Art. 15-21).',
tips: [
{ icon: 'info' as const, title: 'Dokumentversionen', description: 'Jede Aenderung an einem Consent-Dokument erzeugt eine neue Version. Aktive Nutzer muessen bei Aenderungen erneut zustimmen.' },
{ icon: 'warning' as const, title: 'DSGVO-Fristen', description: 'Betroffenenrechte (Art. 15-21) haben gesetzliche Fristen. Auskunft: 30 Tage, Loeschung: unverzueglich.' },
],
},
'notfallplan': {
title: 'Notfallplan & Breach Response',
description: 'Verwalten Sie Ihr Datenpannen-Management nach Art. 33/34 DSGVO',
explanation: 'Der Notfallplan definiert Ihren Prozess bei Datenpannen gemaess Art. 33/34 DSGVO. Er umfasst die 72-Stunden-Meldepflicht an die Aufsichtsbehoerde, die Benachrichtigung betroffener Personen bei hohem Risiko, Incident-Klassifizierung, Eskalationswege und Dokumentationspflichten.',
tips: [
{ icon: 'warning' as const, title: '72-Stunden-Frist', description: 'Art. 33 DSGVO: Meldung an die Aufsichtsbehoerde innerhalb von 72 Stunden nach Bekanntwerden. Verspaetete Meldungen muessen begruendet werden.' },
{ icon: 'info' as const, title: 'Dokumentationspflicht', description: 'Art. 33 Abs. 5: Alle Datenpannen muessen dokumentiert werden — auch solche, die nicht meldepflichtig sind. Die Dokumentation muss der Aufsichtsbehoerde auf Verlangen vorgelegt werden koennen.' },
],
},
'academy': {
title: 'Compliance Academy',
description: 'E-Learning-Plattform fuer Mitarbeiterschulungen',
explanation: 'Die Compliance Academy ermoeglicht KI-generierte Schulungsvideos mit interaktiven Quizfragen und PDF-Zertifikaten. Unternehmen muessen Mitarbeiter regelmaessig in Datenschutz, IT-Sicherheit und KI-Kompetenz schulen (DSGVO Art. 39 Abs. 1 lit. b, EU AI Act Art. 4).',
tips: [
{ icon: 'info' as const, title: 'Schulungspflicht', description: 'DSGVO Art. 39 Abs. 1 lit. b verpflichtet den DSB zur Sensibilisierung und Schulung aller Mitarbeiter.' },
{ icon: 'lightbulb' as const, title: 'Zertifikate', description: 'Schulungszertifikate dienen als Audit-Nachweis. Sie dokumentieren Teilnahme, Testergebnis und Gueltigkeit.' },
],
},
'whistleblower': {
title: 'Hinweisgebersystem',
description: 'Interne Meldestelle gemaess Hinweisgeberschutzgesetz (HinSchG) — seit 17. Dezember 2023 Pflicht fuer alle Unternehmen ab 50 Beschaeftigten',
explanation: 'Das Hinweisgebersystem implementiert eine HinSchG-konforme interne Meldestelle fuer die sichere, auch anonyme Meldung von Rechtsverstoessen. Es setzt die EU-Whistleblowing-Richtlinie (2019/1937) in deutsches Recht um. Beschaeftigungsgeber mit mindestens 50 Beschaeftigten sind zur Einrichtung verpflichtet (§ 12 HinSchG). Das System unterstuetzt den gesamten Meldeprozess: Einreichung, Eingangsbestaetigung (7-Tage-Frist), Sachverhaltspruefung, Folgemaßnahmen und Rueckmeldung (3-Monate-Frist).',
tips: [
{ icon: 'warning' as const, title: 'Pflicht ab 50 Beschaeftigten', description: 'Seit 17.12.2023 gilt die Pflicht fuer ALLE Unternehmen ab 50 Beschaeftigten (§ 12 HinSchG). Verstoesse koennen mit Bussgeldern bis zu 50.000 EUR geahndet werden (§ 40 HinSchG).' },
{ icon: 'info' as const, title: 'Anonymitaet & Vertraulichkeit', description: 'Die Identitaet des Hinweisgebers ist streng vertraulich zu behandeln (§ 8 HinSchG). Anonyme Meldungen sollen bearbeitet werden. Repressalien sind verboten und loesen Schadensersatzpflicht aus (§ 36, § 37 HinSchG).' },
{ icon: 'lightbulb' as const, title: 'Gesetzliche Fristen', description: 'Eingangsbestaetigung innerhalb von 7 Tagen (§ 17 Abs. 1 S. 2). Rueckmeldung ueber ergriffene Folgemaßnahmen innerhalb von 3 Monaten nach Eingangsbestaetigung (§ 17 Abs. 2). Die Dokumentation muss 3 Jahre aufbewahrt werden (§ 11 HinSchG).' },
{ icon: 'warning' as const, title: 'Sachlicher Anwendungsbereich', description: 'Erfasst werden Verstoesse gegen EU-Recht und nationales Recht, u.a. Strafrecht, Datenschutz (DSGVO/BDSG), Arbeitsschutz, Umweltschutz, Geldwaesche, Produktsicherheit und Verbraucherschutz (§ 2 HinSchG).' },
],
},
'incidents': {
title: 'Vorfallmanagement',
description: 'Erfassung und Nachverfolgung von Compliance-Vorfaellen',
explanation: 'Das Vorfallmanagement dokumentiert Compliance-Vorfaelle, Datenpannen und Sicherheitsereignisse. Es unterstuetzt die Meldepflicht nach Art. 33/34 DSGVO und die systematische Ursachenanalyse.',
tips: [
{ icon: 'warning' as const, title: '72-Stunden-Frist', description: 'Datenpannen muessen innerhalb von 72 Stunden an die Aufsichtsbehoerde gemeldet werden (Art. 33 DSGVO).' },
{ icon: 'info' as const, title: 'Klassifizierung', description: 'Vorfaelle werden nach Schweregrad klassifiziert: Niedrig, Mittel, Hoch, Kritisch. Die Klassifizierung bestimmt die Eskalationswege.' },
],
},
'dsb-portal': {
title: 'DSB Portal',
description: 'Arbeitsbereich fuer den Datenschutzbeauftragten',
explanation: 'Das DSB Portal bietet dem Datenschutzbeauftragten einen zentralen Arbeitsbereich mit Aufgabenuebersicht, Beratungsprotokollen und Taetigkeitsberichten. Es unterstuetzt die Aufgaben nach Art. 39 DSGVO.',
tips: [
{ icon: 'info' as const, title: 'Taetigkeitsbericht', description: 'Der DSB muss regelmaessig ueber seine Taetigkeiten berichten. Das Portal generiert strukturierte Berichte.' },
{ icon: 'lightbulb' as const, title: 'Beratungsprotokolle', description: 'Dokumentieren Sie alle Beratungen, um die Rechenschaftspflicht zu erfuellen.' },
],
},
'industry-templates': {
title: 'Branchenvorlagen',
description: 'Branchenspezifische Compliance-Vorlagen und Best Practices',
explanation: 'Branchenvorlagen bieten vorkonfigurierte Compliance-Pakete fuer verschiedene Branchen (Gesundheitswesen, Finanzwesen, E-Commerce etc.). Sie enthalten typische Verarbeitungen, Risiken und Massnahmen.',
tips: [
{ icon: 'lightbulb' as const, title: 'Schnellstart', description: 'Branchenvorlagen beschleunigen die Ersteinrichtung erheblich. Sie koennen spaeter individuell angepasst werden.' },
{ icon: 'info' as const, title: 'Branchenstandards', description: 'Templates beruecksichtigen branchenspezifische Regulierungen wie PCI-DSS (Finanzen) oder Patientendatenschutz (Gesundheit).' },
],
},
'multi-tenant': {
title: 'Multi-Tenant Verwaltung',
description: 'Mandantenverwaltung fuer mehrere Unternehmen oder Standorte',
explanation: 'Die Multi-Tenant Verwaltung ermoeglicht die zentrale Steuerung mehrerer Mandanten (Tochtergesellschaften, Standorte, Kunden). Jeder Mandant hat eigene Compliance-Daten, kann aber zentral verwaltet werden.',
tips: [
{ icon: 'info' as const, title: 'Datentrennung', description: 'Mandantendaten sind strikt getrennt. Nur der uebergeordnete Administrator kann mandantenuebergreifend auswerten.' },
{ icon: 'lightbulb' as const, title: 'Template-Vererbung', description: 'Richtlinien und Vorlagen koennen zentral erstellt und an Mandanten vererbt werden.' },
],
},
'sso': {
title: 'Single Sign-On',
description: 'SSO-Integration und Authentifizierung verwalten',
explanation: 'Die SSO-Konfiguration ermoeglicht die Integration mit Ihrem Identity Provider (SAML, OIDC). Mitarbeiter koennen sich mit ihren bestehenden Unternehmens-Credentials anmelden.',
tips: [
{ icon: 'info' as const, title: 'Unterstuetzte Protokolle', description: 'SAML 2.0 und OpenID Connect (OIDC) werden unterstuetzt. Die gaengigsten IdPs (Azure AD, Okta, Google) sind vorkonfiguriert.' },
{ icon: 'warning' as const, title: 'Sicherheit', description: 'SSO reduziert das Risiko schwacher Passwoerter und ermoeglicht zentrale Zugriffskontrolle.' },
],
},
'document-crawler': {
title: 'Dokumenten-Crawler',
description: 'Automatische Erfassung und Analyse von Compliance-Dokumenten',
explanation: 'Der Dokumenten-Crawler durchsucht Ihre Systeme automatisch nach relevanten Compliance-Dokumenten (Datenschutzerklaerungen, Vertraege, Richtlinien) und analysiert deren Aktualitaet und Vollstaendigkeit.',
tips: [
{ icon: 'lightbulb' as const, title: 'Automatisierung', description: 'Der Crawler erkennt veraltete Dokumente und fehlende Pflichtangaben automatisch.' },
{ icon: 'info' as const, title: 'Quellen', description: 'Unterstuetzt Webseiten, SharePoint, Confluence und lokale Dateisysteme als Datenquellen.' },
],
},
'advisory-board': {
title: 'Compliance-Beirat',
description: 'Virtueller Compliance-Beirat mit KI-Experten',
explanation: 'Der Compliance-Beirat simuliert ein Expertengremium aus verschiedenen Fachrichtungen (Datenschutzrecht, IT-Sicherheit, KI-Ethik). Holen Sie sich Einschaetzungen zu komplexen Compliance-Fragen.',
tips: [
{ icon: 'lightbulb' as const, title: 'Zweitmeinung', description: 'Nutzen Sie den Beirat fuer eine zweite Einschaetzung bei schwierigen Compliance-Entscheidungen.' },
{ icon: 'warning' as const, title: 'Kein Rechtsersatz', description: 'Der KI-Beirat ersetzt keine professionelle Rechtsberatung. Bei kritischen Entscheidungen ziehen Sie einen Anwalt hinzu.' },
],
},
'reporting': {
title: 'Management Reporting',
description: 'Compliance-Berichte und KPIs fuer das Top Management',
explanation: 'Das Executive Reporting Dashboard bietet einen umfassenden Ueberblick ueber den Compliance-Status Ihres Unternehmens. Es aggregiert Daten aus allen Modulen (DSGVO, Lieferanten, Vorfaelle, Schulungen) zu einem Gesamt-Compliance-Score mit Risikobewertung und Fristenuebersicht.',
tips: [
{ icon: 'lightbulb' as const, title: 'Regelmaessig pruefen', description: 'Praesentieren Sie den Compliance-Bericht regelmaessig der Geschaeftsleitung (empfohlen: monatlich oder quartalsweise).' },
{ icon: 'warning' as const, title: 'Rechenschaftspflicht', description: 'Art. 5 Abs. 2 DSGVO verlangt den Nachweis der Compliance. Dieser Bericht dient als Dokumentation gegenueber Aufsichtsbehoerden.' },
],
},
'email-templates': {
title: 'E-Mail-Templates',
description: 'Verwalten Sie Vorlagen fuer alle DSGVO-relevanten Benachrichtigungen',
explanation: 'E-Mail-Templates definieren die Texte und das Layout fuer automatisierte DSGVO-Benachrichtigungen: Einwilligungsbestaetigung, Widerrufsbestaetigung, Auskunftsantwort, Loeschbestaetigung und weitere Lifecycle-E-Mails. Alle 16 Template-Typen koennen individuell angepasst und mit Variablen personalisiert werden.',
tips: [
{ icon: 'info' as const, title: '16 Lifecycle-E-Mails', description: 'Von der Registrierungsbestaetigung bis zur Kontoloeschung — alle relevanten Touchpoints sind mit Vorlagen abgedeckt.' },
{ icon: 'warning' as const, title: 'Pflichtangaben', description: 'Stellen Sie sicher, dass jede E-Mail die gesetzlich vorgeschriebenen Angaben enthaelt: Impressum, Datenschutzhinweis und Widerrufsmoeglichkeit.' },
{ icon: 'lightbulb' as const, title: 'Variablen', description: 'Nutzen Sie Platzhalter wie {{name}}, {{email}} und {{company}} fuer automatische Personalisierung.' },
],
},
'use-case-workshop': {
title: 'Use Case Workshop',
description: 'Erfassen und bewerten Sie Ihre KI-Anwendungsfaelle im Workshop-Format',
explanation: 'Im Use Case Workshop erfassen Sie Ihre KI-Anwendungsfaelle strukturiert in einem gefuehrten Prozess. Der Workshop leitet Sie durch Identifikation, Beschreibung, Datenkategorien, Risikobewertung und Stakeholder-Analyse. Die Ergebnisse fliessen direkt in die Compliance-Bewertung ein.',
tips: [
{ icon: 'lightbulb' as const, title: 'Vollstaendigkeit', description: 'Erfassen Sie alle KI-Anwendungsfaelle — auch solche, die nur intern genutzt werden oder sich noch in der Planungsphase befinden.' },
{ icon: 'info' as const, title: 'Stakeholder einbeziehen', description: 'Beziehen Sie Fachbereiche und IT in den Workshop ein, um alle Anwendungsfaelle zu identifizieren.' },
{ icon: 'warning' as const, title: 'Risikobewertung', description: 'Jeder Anwendungsfall wird nach EU AI Act Risikostufen klassifiziert. Hochrisiko-Systeme erfordern zusaetzliche Dokumentation.' },
],
},
}

View File

@@ -3,7 +3,8 @@
import React, { useState } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { useSDK, getStepById, getNextStep, getPreviousStep, SDKStep, SDK_STEPS } from '@/lib/sdk'
import { useSDK, getStepById, getNextStep, getPreviousStep, SDK_STEPS } from '@/lib/sdk'
import { STEP_EXPLANATIONS } from './StepExplanations'
// =============================================================================
// TYPES
@@ -299,816 +300,4 @@ export function StepHeader({
)
}
// =============================================================================
// STEP EXPLANATION PRESETS
// =============================================================================
export const STEP_EXPLANATIONS = {
'company-profile': {
title: 'Unternehmensprofil',
description: 'Erfassen Sie Ihr Geschäftsmodell und Ihre Zielmärkte',
explanation: 'Im Unternehmensprofil erfassen wir grundlegende Informationen zu Ihrem Unternehmen: Geschäftsmodell (B2B/B2C), Angebote, Firmengröße und Zielmärkte. Diese Informationen helfen uns, die für Sie relevanten Regulierungen zu identifizieren und ehrlich zu kommunizieren, wo unsere Grenzen liegen.',
tips: [
{
icon: 'lightbulb' as const,
title: 'Ehrliche Einschätzung',
description: 'Wir zeigen Ihnen transparent, welche Regulierungen wir abdecken und wann Sie einen Anwalt hinzuziehen sollten.',
},
{
icon: 'info' as const,
title: 'Zielmärkte',
description: 'Je nach Zielmarkt (Deutschland, DACH, EU, weltweit) gelten unterschiedliche Datenschutzgesetze.',
},
],
},
'compliance-scope': {
title: 'Compliance Scope',
description: 'Umfang und Tiefe Ihrer Compliance-Dokumentation bestimmen',
explanation: 'Die Compliance Scope Engine bestimmt deterministisch, welche Dokumente Sie in welcher Tiefe benoetigen. Basierend auf 35 Fragen in 6 Bloecken werden Risiko-, Komplexitaets- und Assurance-Scores berechnet, die in ein 4-Level-Modell (L1 Lean bis L4 Zertifizierungsbereit) muenden.',
tips: [
{
icon: 'lightbulb' as const,
title: 'Deterministisch',
description: 'Alle Entscheidungen sind nachvollziehbar — keine KI, keine Black Box. Jede Einstufung wird mit Rechtsgrundlage und Audit-Trail begruendet.',
},
{
icon: 'info' as const,
title: '4-Level-Modell',
description: 'L1 (Lean Startup) bis L4 (Zertifizierungsbereit). Hard Triggers (Art. 9, Minderjaehrige, Zertifizierungsziele) heben das Level automatisch an.',
},
{
icon: 'warning' as const,
title: 'Hard Triggers',
description: '50 deterministische Regeln pruefen besondere Kategorien (Art. 9), Minderjaehrige, KI-Einsatz, Drittlandtransfers und Zertifizierungsziele.',
},
],
},
'use-case-assessment': {
title: 'Anwendungsfall-Erfassung',
description: 'Erfassen Sie Ihre KI-Anwendungsfälle systematisch',
explanation: 'In der Anwendungsfall-Erfassung dokumentieren Sie Ihre KI-Anwendungsfälle in 5 Schritten: Grunddaten, Datenkategorien, Risikobewertung, Stakeholder und Compliance-Anforderungen. Dies bildet die Basis für alle weiteren Compliance-Maßnahmen.',
tips: [
{
icon: 'lightbulb' as const,
title: 'Tipp: Vollständigkeit',
description: 'Je detaillierter Sie den Anwendungsfall beschreiben, desto besser kann das System passende Compliance-Anforderungen ableiten.',
},
{
icon: 'info' as const,
title: 'Mehrere Anwendungsfälle',
description: 'Sie können mehrere Anwendungsfälle erfassen. Jeder wird separat bewertet und durchläuft den Compliance-Prozess.',
},
],
},
'screening': {
title: 'System Screening',
description: 'Analysieren Sie Ihre Systemlandschaft auf Schwachstellen',
explanation: 'Das System Screening generiert eine Software Bill of Materials (SBOM) und fuehrt einen Security-Scan durch. So erkennen Sie Schwachstellen in Ihren Abhaengigkeiten fruehzeitig.',
tips: [
{
icon: 'warning' as const,
title: 'Kritische Schwachstellen',
description: 'CVEs mit CVSS >= 7.0 sollten priorisiert behandelt werden. Diese werden automatisch in den Security Backlog uebernommen.',
},
{
icon: 'info' as const,
title: 'SBOM-Format',
description: 'Die SBOM wird im CycloneDX-Format generiert und kann fuer Audits exportiert werden.',
},
],
},
'modules': {
title: 'Compliance Module',
description: 'Waehlen Sie die relevanten Regulierungen fuer Ihr Unternehmen',
explanation: 'Compliance-Module sind vordefinierte Regelwerke (z.B. DSGVO, AI Act, ISO 27001). Durch die Aktivierung eines Moduls werden automatisch die zugehoerigen Anforderungen und Kontrollen geladen.',
tips: [
{
icon: 'lightbulb' as const,
title: 'Modul-Auswahl',
description: 'Aktivieren Sie nur Module, die fuer Ihr Unternehmen relevant sind. Weniger ist oft mehr - fokussieren Sie sich auf die wichtigsten Regulierungen.',
},
{
icon: 'info' as const,
title: 'Abhaengigkeiten',
description: 'Manche Module haben Ueberschneidungen. Das System erkennt dies automatisch und vermeidet doppelte Anforderungen.',
},
],
},
'requirements': {
title: 'Anforderungen',
description: 'Pruefen und verwalten Sie die Compliance-Anforderungen',
explanation: 'Anforderungen sind konkrete Vorgaben aus den aktivierten Modulen. Jede Anforderung verweist auf einen Gesetzesartikel und muss durch Kontrollen abgedeckt werden. Vollstaendige CRUD-Operationen mit Backend-Persistenz.',
tips: [
{
icon: 'warning' as const,
title: 'Kritische Anforderungen',
description: 'Anforderungen mit Kritikalitaet "HOCH" sollten priorisiert werden, da Verstoesse zu hohen Bussgeldern fuehren koennen.',
},
{
icon: 'success' as const,
title: 'Status-Workflow',
description: 'Anforderungen durchlaufen: Nicht begonnen → In Bearbeitung → Implementiert → Verifiziert. Bei Backend-Fehler erfolgt automatischer Rollback.',
},
{
icon: 'lightbulb' as const,
title: 'CRUD-Operationen',
description: 'Anforderungen koennen erstellt, bearbeitet und geloescht werden. Alle Aenderungen werden sofort im Backend persistiert.',
},
],
},
'controls': {
title: 'Kontrollen',
description: 'Definieren Sie technische und organisatorische Massnahmen',
explanation: 'Kontrollen (auch TOMs genannt) sind konkrete Massnahmen zur Erfuellung der Anforderungen. Sie koennen praeventiv, detektiv oder korrektiv sein. Evidence-Linking zeigt verknuepfte Nachweise mit Gueltigkeits-Badge.',
tips: [
{
icon: 'lightbulb' as const,
title: 'Wirksamkeit',
description: 'Bewerten Sie die Wirksamkeit jeder Kontrolle. Eine hohe Wirksamkeit (>80%) reduziert das Restrisiko erheblich.',
},
{
icon: 'info' as const,
title: 'Verantwortlichkeiten',
description: 'Weisen Sie jeder Kontrolle einen Verantwortlichen zu. Dies ist fuer Audits wichtig.',
},
{
icon: 'success' as const,
title: 'Evidence-Linking',
description: 'Verknuepfen Sie Nachweise direkt mit Controls. Gueltige, abgelaufene und ausstehende Nachweise werden mit Badges angezeigt.',
},
],
},
'evidence': {
title: 'Nachweise',
description: 'Dokumentieren Sie die Umsetzung mit Belegen',
explanation: 'Nachweise sind Dokumente, Screenshots oder Berichte, die belegen, dass Kontrollen implementiert sind. Server-seitige Pagination fuer grosse Nachweis-Sammlungen.',
tips: [
{
icon: 'warning' as const,
title: 'Gueltigkeit',
description: 'Achten Sie auf das Ablaufdatum von Nachweisen. Abgelaufene Zertifikate oder Berichte muessen erneuert werden. Status: valid, expired, pending, failed.',
},
{
icon: 'success' as const,
title: 'Verknuepfung',
description: 'Verknuepfen Sie Nachweise direkt mit den zugehoerigen Kontrollen fuer eine lueckenlose Dokumentation.',
},
{
icon: 'info' as const,
title: 'Pagination',
description: 'Bei vielen Nachweisen wird automatisch paginiert. Nutzen Sie die Seitennavigation am Ende der Liste.',
},
],
},
'audit-checklist': {
title: 'Audit-Checkliste',
description: 'Systematische Pruefung der Compliance-Konformitaet',
explanation: 'Die Audit-Checkliste wird automatisch aus den Anforderungen generiert. Session-Management mit Sign-Off-Workflow und PDF-Export.',
tips: [
{
icon: 'lightbulb' as const,
title: 'Regelmaessige Pruefung',
description: 'Fuehren Sie die Checkliste mindestens jaehrlich durch, um Compliance-Luecken fruehzeitig zu erkennen.',
},
{
icon: 'info' as const,
title: 'Sign-Off & PDF',
description: 'Zeichnen Sie Pruefpunkte mit digitalem Hash (SHA-256) ab. Exportieren Sie den Report als PDF in Deutsch oder Englisch.',
},
{
icon: 'success' as const,
title: 'Session-History',
description: 'Vergangene Audit-Sitzungen werden mit Status-Badges angezeigt: Draft, In Progress, Completed, Archived.',
},
],
},
'risks': {
title: 'Risiko-Matrix',
description: 'Bewerten und priorisieren Sie Ihre Compliance-Risiken',
explanation: 'Die 5x5 Risiko-Matrix visualisiert Ihre Risiken nach Wahrscheinlichkeit und Auswirkung. Inherent Risk vs. Residual Risk mit visuellem Vergleich.',
tips: [
{
icon: 'warning' as const,
title: 'Kritische Risiken',
description: 'Risiken mit Score >= 20 sind CRITICAL (rot), >= 12 HIGH (orange), >= 6 MEDIUM (gelb), < 6 LOW (gruen).',
},
{
icon: 'success' as const,
title: 'Mitigation',
description: 'Verknuepfen Sie Controls als Mitigationsmassnahmen. Der Residual-Risk wird automatisch anhand verknuepfter Controls berechnet.',
},
{
icon: 'info' as const,
title: 'Status-Workflow',
description: 'Risiken durchlaufen: Identifiziert → Bewertet → Mitigiert → Akzeptiert → Geschlossen.',
},
],
},
'ai-act': {
title: 'AI Act Klassifizierung',
description: 'Registrieren und klassifizieren Sie Ihre KI-Systeme',
explanation: 'Der EU AI Act klassifiziert KI-Systeme in Risikostufen: Minimal, Begrenzt, Hoch und Verboten. KI-Systeme werden im Backend persistent gespeichert und koennen automatisch bewertet werden.',
tips: [
{
icon: 'warning' as const,
title: 'Hochrisiko-Systeme',
description: 'Hochrisiko-KI erfordert 8 Pflichten: Risikomanagement, Daten-Governance, Dokumentation, Transparenz, menschliche Aufsicht, Genauigkeit, Robustheit, Cybersicherheit.',
},
{
icon: 'lightbulb' as const,
title: 'Automatische Bewertung',
description: 'Nutzen Sie die Assess-Funktion: Sie analysiert Zweck und Sektor und leitet die Risikokategorie + Pflichten automatisch ab.',
},
{
icon: 'info' as const,
title: 'CRUD-Operationen',
description: 'KI-Systeme koennen registriert, bearbeitet, bewertet und geloescht werden. Alle Daten werden backend-persistent gespeichert.',
},
],
},
'dsfa': {
title: 'Datenschutz-Folgenabschaetzung',
description: 'Erstellen Sie eine DSFA fuer Hochrisiko-Verarbeitungen',
explanation: 'Eine DSFA (Art. 35 DSGVO) ist erforderlich, wenn eine Verarbeitung voraussichtlich hohe Risiken fuer Betroffene birgt. Das Tool fuehrt Sie durch alle erforderlichen Abschnitte.',
tips: [
{
icon: 'warning' as const,
title: 'Pflicht',
description: 'Eine DSFA ist Pflicht bei: Profiling mit rechtlicher Wirkung, umfangreicher Verarbeitung besonderer Datenkategorien, systematischer Ueberwachung.',
},
{
icon: 'lightbulb' as const,
title: 'Konsultation',
description: 'Bei hohem Restrisiko muss die Aufsichtsbehoerde konsultiert werden (Art. 36 DSGVO).',
},
],
},
'tom': {
title: 'Technische und Organisatorische Massnahmen',
description: 'TOMs nach Art. 32 DSGVO mit Vendor-Controls-Querverweis',
explanation: 'TOMs sind konkrete Sicherheitsmassnahmen zum Schutz personenbezogener Daten. Das Dashboard zeigt den Status aller aus dem TOM Generator abgeleiteten Massnahmen mit SDM-Mapping und Gap-Analyse. Im Uebersicht-Tab werden zusaetzlich Vendor-TOM-Controls (VND-TOM-01 bis VND-TOM-06) aus dem Vendor-Compliance-Modul als Querverweis angezeigt.',
tips: [
{
icon: 'warning' as const,
title: 'Nachweispflicht',
description: 'TOMs muessen nachweisbar real sein. Verknuepfen Sie Evidence-Dokumente (Policies, Zertifikate, Screenshots) mit jeder Massnahme, um die Rechenschaftspflicht (Art. 5 Abs. 2 DSGVO) zu erfuellen.',
},
{
icon: 'info' as const,
title: 'Generator nutzen',
description: 'Der 6-Schritt-Wizard leitet TOMs systematisch aus Ihrem Risikoprofil ab. Starten Sie dort, um eine vollstaendige Baseline zu erhalten.',
},
{
icon: 'info' as const,
title: 'SDM-Mapping',
description: 'Kontrollen werden den 7 SDM-Gewaehrleistungszielen zugeordnet: Verfuegbarkeit, Integritaet, Vertraulichkeit, Nichtverkettung, Intervenierbarkeit, Transparenz, Datenminimierung.',
},
{
icon: 'success' as const,
title: 'Vendor-Controls',
description: 'Im Uebersicht-Tab werden Vendor-TOM-Controls (VND-TOM-01 bis 06) als Read-Only-Querverweis angezeigt: Verschluesselung, Zugriffskontrolle, Verfuegbarkeit und Ueberpruefungsverfahren Ihrer Auftragsverarbeiter.',
},
],
},
'vvt': {
title: 'Verarbeitungsverzeichnis',
description: 'Verarbeitungsverzeichnis nach Art. 30 DSGVO mit integriertem Processor-Tab',
explanation: 'Das Verarbeitungsverzeichnis (VVT) dokumentiert alle Verarbeitungstaetigkeiten mit personenbezogenen Daten. Der integrierte Generator-Fragebogen befuellt 70-90% der Pflichtfelder automatisch. Der Tab "Auftragsverarbeiter (Abs. 2)" liest Vendors mit role=PROCESSOR/SUB_PROCESSOR direkt aus der Vendor-Compliance-API — keine doppelte Datenhaltung.',
tips: [
{
icon: 'warning' as const,
title: 'Pflicht fuer alle',
description: 'Die Ausnahme fuer Unternehmen <250 Mitarbeiter greift nur bei gelegentlicher, risikoarmer Verarbeitung ohne besondere Kategorien (Art. 30 Abs. 5).',
},
{
icon: 'info' as const,
title: 'Zweck-zuerst',
description: 'Definieren Sie Verarbeitungen nach Geschaeftszweck, nicht nach Tool. Ein Tool kann mehrere Verarbeitungen abdecken, eine Verarbeitung kann mehrere Tools nutzen.',
},
{
icon: 'info' as const,
title: 'Kein oeffentliches Dokument',
description: 'Das VVT ist ein internes Dokument. Es muss der Aufsichtsbehoerde nur auf Verlangen vorgelegt werden (Art. 30 Abs. 4).',
},
{
icon: 'success' as const,
title: 'Processor-Tab (Art. 30 Abs. 2)',
description: 'Auftragsverarbeiter werden direkt aus dem Vendor Register gelesen (Read-Only). Neue Vendors werden im Vendor-Compliance-Modul angelegt und erscheinen hier automatisch. PDF-Druck fuer Art. 30 Abs. 2 Dokument.',
},
],
},
'cookie-banner': {
title: 'Cookie Banner',
description: 'Konfigurieren Sie einen DSGVO-konformen Cookie Banner mit persistenter DB-Speicherung',
explanation: 'Der Cookie Banner Generator erstellt einen rechtssicheren Banner mit Opt-In fuer nicht-essentielle Cookies. Alle Einstellungen — einschliesslich Ueberschrift, Beschreibung und Datenschutz-Link — werden in der Datenbank gespeichert und bleiben auch nach einem Neustart erhalten. Der generierte Embed-Code wird direkt aus der gespeicherten Konfiguration erzeugt.',
tips: [
{
icon: 'warning' as const,
title: 'Opt-In Pflicht',
description: 'Fuer Marketing- und Analytics-Cookies ist eine aktive Einwilligung erforderlich. Vorangekreuzte Checkboxen sind nicht erlaubt.',
},
{
icon: 'lightbulb' as const,
title: 'Design und Texte',
description: 'Passen Sie Ueberschrift, Beschreibung und Farben an Ihr Corporate Design an. Aenderungen werden in der Vorschau sofort sichtbar.',
},
{
icon: 'info' as const,
title: 'Embed-Code',
description: 'Der Code exportiert einen vollstaendigen HTML+CSS+JS-Block aus Ihrer gespeicherten Konfiguration — einfach vor dem schliessenden </body>-Tag einbinden.',
},
],
},
'obligations': {
title: 'Pflichtenuebersicht',
description: 'Regulatorische Pflichten mit 12 Compliance-Checks und Vendor-Verknuepfung',
explanation: 'Die Pflichtenuebersicht aggregiert alle Anforderungen aus DSGVO, AI Act, NIS2 und weiteren Regulierungen. 12 automatische Compliance-Checks pruefen Vollstaendigkeit, Fristen, Nachweise und Vendor-Verknuepfungen. Art.-28-Pflichten koennen mit Auftragsverarbeitern aus dem Vendor Register verknuepft werden. Das Pflichtenregister-Dokument (11 Sektionen) kann als auditfaehiges PDF gedruckt werden.',
tips: [
{
icon: 'info' as const,
title: 'Filterung',
description: 'Filtern Sie nach Regulierung, Prioritaet oder Status, um die relevanten Pflichten schnell zu finden.',
},
{
icon: 'warning' as const,
title: 'Compliance-Checks',
description: '12 automatische Checks: Fehlende Verantwortliche, ueberfaellige Fristen, fehlende Nachweise, keine Rechtsreferenz, stagnierende Regulierungen, nicht gestartete High-Priority-Pflichten, fehlende Vendor-Verknuepfung (Art. 28) u.v.m.',
},
{
icon: 'success' as const,
title: 'Vendor-Verknuepfung',
description: 'Art.-28-Pflichten (Auftragsverarbeitung) koennen direkt mit Vendors aus dem Vendor Register verknuepft werden. Check #12 (MISSING_VENDOR_LINK) warnt bei fehlender Verknuepfung.',
},
{
icon: 'lightbulb' as const,
title: 'Pflichtenregister-Dokument',
description: 'Generieren Sie ein auditfaehiges Pflichtenregister mit 11 Sektionen: Ziel, Geltungsbereich, Methodik, Regulatorische Grundlagen, Pflichtenuebersicht, Details, Verantwortlichkeiten, Fristen, Nachweisverzeichnis, Compliance-Status und Aenderungshistorie.',
},
],
},
'loeschfristen': {
title: 'Loeschfristen',
description: 'Aufbewahrungsrichtlinien mit VVT-Verknuepfung und Vendor-Zuordnung',
explanation: 'Loeschfristen legen fest, wie lange personenbezogene Daten gespeichert werden duerfen. Die 3-Stufen-Logik (Zweckende, Aufbewahrungspflicht, Legal Hold) stellt sicher, dass alle gesetzlichen Anforderungen beruecksichtigt werden. Policies koennen mit VVT-Verarbeitungstaetigkeiten und Auftragsverarbeitern aus dem Vendor Register verknuepft werden.',
tips: [
{
icon: 'warning' as const,
title: '3-Stufen-Logik',
description: 'Jede Loeschfrist folgt einer 3-Stufen-Logik: 1. Zweckende (Daten werden nach Zweckwegfall geloescht), 2. Aufbewahrungspflicht (gesetzliche Fristen verhindern Loeschung), 3. Legal Hold (laufende Verfahren blockieren Loeschung).',
},
{
icon: 'info' as const,
title: 'Deutsche Rechtsgrundlagen',
description: 'Der Generator kennt die wichtigsten Aufbewahrungstreiber: AO (10 J. Steuer), HGB (10/6 J. Handel), UStG (10 J. Rechnungen), BGB (3 J. Verjaehrung), ArbZG (2 J. Zeiterfassung), AGG (6 Mon. Bewerbungen).',
},
{
icon: 'info' as const,
title: 'Backup-Behandlung',
description: 'Auch Backups muessen ins Loeschkonzept einbezogen werden. Daten koennen nach primaerer Loeschung noch in Backup-Systemen existieren.',
},
{
icon: 'success' as const,
title: 'Vendor-Verknuepfung',
description: 'Loeschfrist-Policies koennen mit Auftragsverarbeitern verknuepft werden. So ist dokumentiert, welche Vendors Loeschpflichten fuer bestimmte Datenkategorien haben.',
},
],
},
'consent': {
title: 'Rechtliche Vorlagen',
description: 'Generieren Sie AGB, Datenschutzerklaerung und Nutzungsbedingungen',
explanation: 'Die rechtlichen Vorlagen werden basierend auf Ihren Verarbeitungstaetigkeiten und Use Cases generiert. Sie sind auf Ihre spezifische Situation zugeschnitten.',
tips: [
{
icon: 'info' as const,
title: 'Anpassung',
description: 'Die generierten Vorlagen koennen und sollten an Ihre spezifischen Anforderungen angepasst werden.',
},
{
icon: 'warning' as const,
title: 'Rechtspruefung',
description: 'Lassen Sie die finalen Dokumente von einem Rechtsanwalt pruefen, bevor Sie sie veroeffentlichen.',
},
],
},
'einwilligungen': {
title: 'Einwilligungen',
description: 'Verwalten Sie Consent-Tracking und Einwilligungsnachweise',
explanation: 'Hier konfigurieren Sie, wie Einwilligungen erfasst, gespeichert und nachgewiesen werden. Dies ist essentiell fuer den Nachweis der Rechtmaessigkeit.',
tips: [
{
icon: 'success' as const,
title: 'Nachweis',
description: 'Speichern Sie fuer jede Einwilligung: Zeitpunkt, Version des Textes, Art der Einwilligung.',
},
{
icon: 'info' as const,
title: 'Widerruf',
description: 'Stellen Sie sicher, dass Nutzer ihre Einwilligung jederzeit widerrufen koennen.',
},
],
},
'dsr': {
title: 'DSR Portal',
description: 'Richten Sie ein Portal fuer Betroffenenrechte ein',
explanation: 'Das DSR (Data Subject Rights) Portal ermoeglicht Betroffenen, ihre Rechte nach DSGVO auszuueben: Auskunft, Loeschung, Berichtigung, Datenportabilitaet.',
tips: [
{
icon: 'warning' as const,
title: 'Fristen',
description: 'Anfragen muessen innerhalb von 30 Tagen beantwortet werden. Richten Sie Workflows ein, um dies sicherzustellen.',
},
{
icon: 'lightbulb' as const,
title: 'Identitaetspruefung',
description: 'Implementieren Sie eine sichere Identitaetspruefung, bevor Sie Daten herausgeben.',
},
],
},
'escalations': {
title: 'Eskalations-Workflows',
description: 'Definieren Sie Management-Workflows fuer Compliance-Vorfaelle',
explanation: 'Eskalations-Workflows legen fest, wie auf Compliance-Vorfaelle reagiert wird: Wer wird informiert, welche Massnahmen werden ergriffen, wie wird dokumentiert.',
tips: [
{
icon: 'info' as const,
title: 'Datenpannen',
description: 'Bei Datenpannen muss die Aufsichtsbehoerde innerhalb von 72 Stunden informiert werden.',
},
{
icon: 'success' as const,
title: 'Verantwortlichkeiten',
description: 'Definieren Sie klare Verantwortlichkeiten fuer jeden Schritt im Eskalationsprozess.',
},
],
},
'vendor-compliance': {
title: 'Vendor Compliance',
description: 'Auftragsverarbeiter-Management mit Cross-Modul-Integration',
explanation: 'Vendor Compliance verwaltet alle Auftragsverarbeiter (Art. 28 DSGVO) und Drittanbieter. Fuer jeden Vendor werden AVVs, Drittlandtransfers, TOMs und Subunternehmer geprueft. Das Modul ist zentral mit vier weiteren Modulen integriert: VVT-Processor-Tab liest Vendors direkt aus der API, Obligations und Loeschfristen verknuepfen Vendors ueber linked_vendor_ids, TOM zeigt Vendor-Controls als Querverweis.',
tips: [
{
icon: 'warning' as const,
title: 'Art. 28 DSGVO',
description: 'Jede Auftragsverarbeitung erfordert einen schriftlichen Vertrag (AVV). Pruefen Sie: Weisungsgebundenheit, TOMs, Subunternehmer-Genehmigung, Loeschpflicht und Audit-Recht.',
},
{
icon: 'info' as const,
title: 'Cross-Modul-Integration',
description: 'Vendors erscheinen automatisch im VVT-Processor-Tab, koennen in Obligations und Loeschfristen verknuepft werden, und ihre TOM-Controls werden im TOM-Modul als Querverweis angezeigt.',
},
{
icon: 'lightbulb' as const,
title: 'Drittlandtransfer',
description: 'Bei Datenverarbeitung ausserhalb der EU/EWR sind Standardvertragsklauseln (SCCs) oder andere Garantien nach Art. 44-49 DSGVO erforderlich.',
},
{
icon: 'success' as const,
title: 'Controls Library',
description: '6 TOM-Domain Controls (VND-TOM-01 bis VND-TOM-06) pruefen Verschluesselung, Zugriffskontrolle, Verfuegbarkeit und Ueberpruefungsverfahren bei Ihren Auftragsverarbeitern.',
},
],
},
'document-generator': {
title: 'Dokumentengenerator',
description: 'Generieren Sie rechtliche Dokumente aus lizenzkonformen Vorlagen',
explanation: 'Der Dokumentengenerator nutzt frei lizenzierte Textbausteine (CC0, MIT, CC BY 4.0) um Datenschutzerklaerungen, AGB, Cookie-Banner und andere rechtliche Dokumente zu erstellen. Die Quellen werden mit korrekter Lizenz-Compliance und Attribution gehandhabt.',
tips: [
{
icon: 'lightbulb' as const,
title: 'Lizenzfreie Vorlagen',
description: 'Alle verwendeten Textbausteine stammen aus lizenzierten Quellen (CC0, MIT, CC BY 4.0). Die Attribution wird automatisch hinzugefuegt.',
},
{
icon: 'info' as const,
title: 'Platzhalter',
description: 'Fuellen Sie die Platzhalter (z.B. [FIRMENNAME], [ADRESSE]) mit Ihren Unternehmensdaten aus.',
},
{
icon: 'warning' as const,
title: 'Rechtspruefung',
description: 'Lassen Sie generierte Dokumente vor der Veroeffentlichung von einem Rechtsanwalt pruefen.',
},
],
},
'source-policy': {
title: 'Source Policy',
description: 'Verwalten Sie Ihre Datenquellen-Governance',
explanation: 'Die Source Policy definiert, welche externen Datenquellen fuer Ihre Anwendung zugelassen sind. Sie umfasst eine Whitelist, Operationsmatrix (Lookup, RAG, Training, Export), PII-Regeln und ein Audit-Trail.',
tips: [
{
icon: 'warning' as const,
title: 'Lizenzierung',
description: 'Pruefen Sie die Lizenzen aller Datenquellen (DL-DE-BY, CC-BY, CC0). Nicht-lizenzierte Quellen koennen rechtliche Risiken bergen.',
},
{
icon: 'info' as const,
title: 'PII-Regeln',
description: 'Definieren Sie klare Regeln fuer den Umgang mit personenbezogenen Daten in externen Quellen.',
},
],
},
'audit-report': {
title: 'Audit Report',
description: 'Erstellen und verwalten Sie Audit-Sitzungen',
explanation: 'Im Audit Report erstellen Sie formelle Audit-Sitzungen. Uebersicht mit Status-Badges, Detail-Seite pro Sitzung mit Fortschrittsbalken und interaktiven Checklist-Items.',
tips: [
{
icon: 'lightbulb' as const,
title: 'Regelmaessigkeit',
description: 'Fuehren Sie mindestens jaehrlich ein formelles Audit durch. Dokumentieren Sie Abweichungen und Massnahmenplaene.',
},
{
icon: 'success' as const,
title: 'Detail-Ansicht',
description: 'Klicken Sie auf eine Sitzung fuer die Detail-Seite: Metadaten, Fortschrittsbalken, Checklist-Items mit Sign-Off und Notizen.',
},
{
icon: 'info' as const,
title: 'PDF-Export',
description: 'Generieren Sie PDF-Reports in Deutsch oder Englisch fuer externe Pruefer und Aufsichtsbehoerden.',
},
],
},
'workflow': {
title: 'Document Workflow',
description: 'Freigabe-Workflow mit Split-View-Editor und DB-persistenter Versionierung',
explanation: 'Der Document Workflow bietet einen Split-View-Editor: links die veroffentlichte Version, rechts der aktuelle Entwurf. Dokumente durchlaufen den Status Draft → Review → Approved → Published. Alle Versionen werden in der Datenbank gespeichert. Word-Dokumente koennen direkt als neue Version importiert werden.',
tips: [
{
icon: 'warning' as const,
title: 'Vier-Augen-Prinzip',
description: 'Rechtliche Dokumente sollten immer von mindestens einer weiteren Person geprueft werden, bevor sie veroeffentlicht werden.',
},
{
icon: 'info' as const,
title: 'Versionierung',
description: 'Jede Aenderung wird als neue Version gespeichert. Veroeffentlichte Versionen sind unveraenderlich — Aenderungen erzeugen stets eine neue Version.',
},
{
icon: 'lightbulb' as const,
title: 'DOCX-Import',
description: 'Bestehende Word-Dokumente koennen direkt hochgeladen und als Basis fuer neue Versionen verwendet werden.',
},
],
},
'consent-management': {
title: 'Consent Verwaltung',
description: 'Verwalten Sie Consent-Dokumente, Versionen und DSGVO-Prozesse',
explanation: 'Die Consent Verwaltung umfasst das Lifecycle-Management Ihrer rechtlichen Dokumente (AGB, Datenschutz, Cookie-Richtlinien), die Verwaltung von E-Mail-Templates (16 Lifecycle-E-Mails) und die Steuerung der DSGVO-Prozesse (Art. 15-21).',
tips: [
{
icon: 'info' as const,
title: 'Dokumentversionen',
description: 'Jede Aenderung an einem Consent-Dokument erzeugt eine neue Version. Aktive Nutzer muessen bei Aenderungen erneut zustimmen.',
},
{
icon: 'warning' as const,
title: 'DSGVO-Fristen',
description: 'Betroffenenrechte (Art. 15-21) haben gesetzliche Fristen. Auskunft: 30 Tage, Loeschung: unverzueglich.',
},
],
},
'notfallplan': {
title: 'Notfallplan & Breach Response',
description: 'Verwalten Sie Ihr Datenpannen-Management nach Art. 33/34 DSGVO',
explanation: 'Der Notfallplan definiert Ihren Prozess bei Datenpannen gemaess Art. 33/34 DSGVO. Er umfasst die 72-Stunden-Meldepflicht an die Aufsichtsbehoerde, die Benachrichtigung betroffener Personen bei hohem Risiko, Incident-Klassifizierung, Eskalationswege und Dokumentationspflichten.',
tips: [
{
icon: 'warning' as const,
title: '72-Stunden-Frist',
description: 'Art. 33 DSGVO: Meldung an die Aufsichtsbehoerde innerhalb von 72 Stunden nach Bekanntwerden. Verspaetete Meldungen muessen begruendet werden.',
},
{
icon: 'info' as const,
title: 'Dokumentationspflicht',
description: 'Art. 33 Abs. 5: Alle Datenpannen muessen dokumentiert werden — auch solche, die nicht meldepflichtig sind. Die Dokumentation muss der Aufsichtsbehoerde auf Verlangen vorgelegt werden koennen.',
},
],
},
'academy': {
title: 'Compliance Academy',
description: 'E-Learning-Plattform fuer Mitarbeiterschulungen',
explanation: 'Die Compliance Academy ermoeglicht KI-generierte Schulungsvideos mit interaktiven Quizfragen und PDF-Zertifikaten. Unternehmen muessen Mitarbeiter regelmaessig in Datenschutz, IT-Sicherheit und KI-Kompetenz schulen (DSGVO Art. 39 Abs. 1 lit. b, EU AI Act Art. 4).',
tips: [
{
icon: 'info' as const,
title: 'Schulungspflicht',
description: 'DSGVO Art. 39 Abs. 1 lit. b verpflichtet den DSB zur Sensibilisierung und Schulung aller Mitarbeiter.',
},
{
icon: 'lightbulb' as const,
title: 'Zertifikate',
description: 'Schulungszertifikate dienen als Audit-Nachweis. Sie dokumentieren Teilnahme, Testergebnis und Gueltigkeit.',
},
],
},
'whistleblower': {
title: 'Hinweisgebersystem',
description: 'Interne Meldestelle gemaess Hinweisgeberschutzgesetz (HinSchG) — seit 17. Dezember 2023 Pflicht fuer alle Unternehmen ab 50 Beschaeftigten',
explanation: 'Das Hinweisgebersystem implementiert eine HinSchG-konforme interne Meldestelle fuer die sichere, auch anonyme Meldung von Rechtsverstoessen. Es setzt die EU-Whistleblowing-Richtlinie (2019/1937) in deutsches Recht um. Beschaeftigungsgeber mit mindestens 50 Beschaeftigten sind zur Einrichtung verpflichtet (§ 12 HinSchG). Das System unterstuetzt den gesamten Meldeprozess: Einreichung, Eingangsbestaetigung (7-Tage-Frist), Sachverhaltspruefung, Folgemaßnahmen und Rueckmeldung (3-Monate-Frist).',
tips: [
{
icon: 'warning' as const,
title: 'Pflicht ab 50 Beschaeftigten',
description: 'Seit 17.12.2023 gilt die Pflicht fuer ALLE Unternehmen ab 50 Beschaeftigten (§ 12 HinSchG). Verstoesse koennen mit Bussgeldern bis zu 50.000 EUR geahndet werden (§ 40 HinSchG).',
},
{
icon: 'info' as const,
title: 'Anonymitaet & Vertraulichkeit',
description: 'Die Identitaet des Hinweisgebers ist streng vertraulich zu behandeln (§ 8 HinSchG). Anonyme Meldungen sollen bearbeitet werden. Repressalien sind verboten und loesen Schadensersatzpflicht aus (§ 36, § 37 HinSchG).',
},
{
icon: 'lightbulb' as const,
title: 'Gesetzliche Fristen',
description: 'Eingangsbestaetigung innerhalb von 7 Tagen (§ 17 Abs. 1 S. 2). Rueckmeldung ueber ergriffene Folgemaßnahmen innerhalb von 3 Monaten nach Eingangsbestaetigung (§ 17 Abs. 2). Die Dokumentation muss 3 Jahre aufbewahrt werden (§ 11 HinSchG).',
},
{
icon: 'warning' as const,
title: 'Sachlicher Anwendungsbereich',
description: 'Erfasst werden Verstoesse gegen EU-Recht und nationales Recht, u.a. Strafrecht, Datenschutz (DSGVO/BDSG), Arbeitsschutz, Umweltschutz, Geldwaesche, Produktsicherheit und Verbraucherschutz (§ 2 HinSchG).',
},
],
},
'incidents': {
title: 'Vorfallmanagement',
description: 'Erfassung und Nachverfolgung von Compliance-Vorfaellen',
explanation: 'Das Vorfallmanagement dokumentiert Compliance-Vorfaelle, Datenpannen und Sicherheitsereignisse. Es unterstuetzt die Meldepflicht nach Art. 33/34 DSGVO und die systematische Ursachenanalyse.',
tips: [
{
icon: 'warning' as const,
title: '72-Stunden-Frist',
description: 'Datenpannen muessen innerhalb von 72 Stunden an die Aufsichtsbehoerde gemeldet werden (Art. 33 DSGVO).',
},
{
icon: 'info' as const,
title: 'Klassifizierung',
description: 'Vorfaelle werden nach Schweregrad klassifiziert: Niedrig, Mittel, Hoch, Kritisch. Die Klassifizierung bestimmt die Eskalationswege.',
},
],
},
'dsb-portal': {
title: 'DSB Portal',
description: 'Arbeitsbereich fuer den Datenschutzbeauftragten',
explanation: 'Das DSB Portal bietet dem Datenschutzbeauftragten einen zentralen Arbeitsbereich mit Aufgabenuebersicht, Beratungsprotokollen und Taetigkeitsberichten. Es unterstuetzt die Aufgaben nach Art. 39 DSGVO.',
tips: [
{
icon: 'info' as const,
title: 'Taetigkeitsbericht',
description: 'Der DSB muss regelmaessig ueber seine Taetigkeiten berichten. Das Portal generiert strukturierte Berichte.',
},
{
icon: 'lightbulb' as const,
title: 'Beratungsprotokolle',
description: 'Dokumentieren Sie alle Beratungen, um die Rechenschaftspflicht zu erfuellen.',
},
],
},
'industry-templates': {
title: 'Branchenvorlagen',
description: 'Branchenspezifische Compliance-Vorlagen und Best Practices',
explanation: 'Branchenvorlagen bieten vorkonfigurierte Compliance-Pakete fuer verschiedene Branchen (Gesundheitswesen, Finanzwesen, E-Commerce etc.). Sie enthalten typische Verarbeitungen, Risiken und Massnahmen.',
tips: [
{
icon: 'lightbulb' as const,
title: 'Schnellstart',
description: 'Branchenvorlagen beschleunigen die Ersteinrichtung erheblich. Sie koennen spaeter individuell angepasst werden.',
},
{
icon: 'info' as const,
title: 'Branchenstandards',
description: 'Templates beruecksichtigen branchenspezifische Regulierungen wie PCI-DSS (Finanzen) oder Patientendatenschutz (Gesundheit).',
},
],
},
'multi-tenant': {
title: 'Multi-Tenant Verwaltung',
description: 'Mandantenverwaltung fuer mehrere Unternehmen oder Standorte',
explanation: 'Die Multi-Tenant Verwaltung ermoeglicht die zentrale Steuerung mehrerer Mandanten (Tochtergesellschaften, Standorte, Kunden). Jeder Mandant hat eigene Compliance-Daten, kann aber zentral verwaltet werden.',
tips: [
{
icon: 'info' as const,
title: 'Datentrennung',
description: 'Mandantendaten sind strikt getrennt. Nur der uebergeordnete Administrator kann mandantenuebergreifend auswerten.',
},
{
icon: 'lightbulb' as const,
title: 'Template-Vererbung',
description: 'Richtlinien und Vorlagen koennen zentral erstellt und an Mandanten vererbt werden.',
},
],
},
'sso': {
title: 'Single Sign-On',
description: 'SSO-Integration und Authentifizierung verwalten',
explanation: 'Die SSO-Konfiguration ermoeglicht die Integration mit Ihrem Identity Provider (SAML, OIDC). Mitarbeiter koennen sich mit ihren bestehenden Unternehmens-Credentials anmelden.',
tips: [
{
icon: 'info' as const,
title: 'Unterstuetzte Protokolle',
description: 'SAML 2.0 und OpenID Connect (OIDC) werden unterstuetzt. Die gaengigsten IdPs (Azure AD, Okta, Google) sind vorkonfiguriert.',
},
{
icon: 'warning' as const,
title: 'Sicherheit',
description: 'SSO reduziert das Risiko schwacher Passwoerter und ermoeglicht zentrale Zugriffskontrolle.',
},
],
},
'document-crawler': {
title: 'Dokumenten-Crawler',
description: 'Automatische Erfassung und Analyse von Compliance-Dokumenten',
explanation: 'Der Dokumenten-Crawler durchsucht Ihre Systeme automatisch nach relevanten Compliance-Dokumenten (Datenschutzerklaerungen, Vertraege, Richtlinien) und analysiert deren Aktualitaet und Vollstaendigkeit.',
tips: [
{
icon: 'lightbulb' as const,
title: 'Automatisierung',
description: 'Der Crawler erkennt veraltete Dokumente und fehlende Pflichtangaben automatisch.',
},
{
icon: 'info' as const,
title: 'Quellen',
description: 'Unterstuetzt Webseiten, SharePoint, Confluence und lokale Dateisysteme als Datenquellen.',
},
],
},
'advisory-board': {
title: 'Compliance-Beirat',
description: 'Virtueller Compliance-Beirat mit KI-Experten',
explanation: 'Der Compliance-Beirat simuliert ein Expertengremium aus verschiedenen Fachrichtungen (Datenschutzrecht, IT-Sicherheit, KI-Ethik). Holen Sie sich Einschaetzungen zu komplexen Compliance-Fragen.',
tips: [
{
icon: 'lightbulb' as const,
title: 'Zweitmeinung',
description: 'Nutzen Sie den Beirat fuer eine zweite Einschaetzung bei schwierigen Compliance-Entscheidungen.',
},
{
icon: 'warning' as const,
title: 'Kein Rechtsersatz',
description: 'Der KI-Beirat ersetzt keine professionelle Rechtsberatung. Bei kritischen Entscheidungen ziehen Sie einen Anwalt hinzu.',
},
],
},
'reporting': {
title: 'Management Reporting',
description: 'Compliance-Berichte und KPIs fuer das Top Management',
explanation: 'Das Executive Reporting Dashboard bietet einen umfassenden Ueberblick ueber den Compliance-Status Ihres Unternehmens. Es aggregiert Daten aus allen Modulen (DSGVO, Lieferanten, Vorfaelle, Schulungen) zu einem Gesamt-Compliance-Score mit Risikobewertung und Fristenuebersicht.',
tips: [
{
icon: 'lightbulb' as const,
title: 'Regelmaessig pruefen',
description: 'Praesentieren Sie den Compliance-Bericht regelmaessig der Geschaeftsleitung (empfohlen: monatlich oder quartalsweise).',
},
{
icon: 'warning' as const,
title: 'Rechenschaftspflicht',
description: 'Art. 5 Abs. 2 DSGVO verlangt den Nachweis der Compliance. Dieser Bericht dient als Dokumentation gegenueber Aufsichtsbehoerden.',
},
],
},
'email-templates': {
title: 'E-Mail-Templates',
description: 'Verwalten Sie Vorlagen fuer alle DSGVO-relevanten Benachrichtigungen',
explanation: 'E-Mail-Templates definieren die Texte und das Layout fuer automatisierte DSGVO-Benachrichtigungen: Einwilligungsbestaetigung, Widerrufsbestaetigung, Auskunftsantwort, Loeschbestaetigung und weitere Lifecycle-E-Mails. Alle 16 Template-Typen koennen individuell angepasst und mit Variablen personalisiert werden.',
tips: [
{
icon: 'info' as const,
title: '16 Lifecycle-E-Mails',
description: 'Von der Registrierungsbestaetigung bis zur Kontoloeschung — alle relevanten Touchpoints sind mit Vorlagen abgedeckt.',
},
{
icon: 'warning' as const,
title: 'Pflichtangaben',
description: 'Stellen Sie sicher, dass jede E-Mail die gesetzlich vorgeschriebenen Angaben enthaelt: Impressum, Datenschutzhinweis und Widerrufsmoeglichkeit.',
},
{
icon: 'lightbulb' as const,
title: 'Variablen',
description: 'Nutzen Sie Platzhalter wie {{name}}, {{email}} und {{company}} fuer automatische Personalisierung.',
},
],
},
'use-case-workshop': {
title: 'Use Case Workshop',
description: 'Erfassen und bewerten Sie Ihre KI-Anwendungsfaelle im Workshop-Format',
explanation: 'Im Use Case Workshop erfassen Sie Ihre KI-Anwendungsfaelle strukturiert in einem gefuehrten Prozess. Der Workshop leitet Sie durch Identifikation, Beschreibung, Datenkategorien, Risikobewertung und Stakeholder-Analyse. Die Ergebnisse fliessen direkt in die Compliance-Bewertung ein.',
tips: [
{
icon: 'lightbulb' as const,
title: 'Vollstaendigkeit',
description: 'Erfassen Sie alle KI-Anwendungsfaelle — auch solche, die nur intern genutzt werden oder sich noch in der Planungsphase befinden.',
},
{
icon: 'info' as const,
title: 'Stakeholder einbeziehen',
description: 'Beziehen Sie Fachbereiche und IT in den Workshop ein, um alle Anwendungsfaelle zu identifizieren.',
},
{
icon: 'warning' as const,
title: 'Risikobewertung',
description: 'Jeder Anwendungsfall wird nach EU AI Act Risikostufen klassifiziert. Hochrisiko-Systeme erfordern zusaetzliche Dokumentation.',
},
],
},
} satisfies Record<string, { title: string; description: string; explanation: string; tips: StepTip[] }>
export default StepHeader

View File

@@ -0,0 +1,225 @@
'use client'
// =============================================================================
// BLOCK 9: Datenkategorien pro Abteilung (aufklappbare Kacheln)
// Extracted from ScopeWizardTab for LOC compliance.
// =============================================================================
import { useState } from 'react'
import type { ScopeProfilingAnswer } from '@/lib/sdk/compliance-scope-types'
import { DEPARTMENT_DATA_CATEGORIES } from '@/lib/sdk/vvt-profiling'
/** Mapping Block 8 vvt_departments values → DEPARTMENT_DATA_CATEGORIES keys */
export const DEPT_VALUE_TO_KEY: Record<string, string[]> = {
personal: ['dept_hr', 'dept_recruiting'],
finanzen: ['dept_finance'],
vertrieb: ['dept_sales'],
marketing: ['dept_marketing'],
it: ['dept_it'],
recht: ['dept_recht'],
kundenservice: ['dept_support'],
produktion: ['dept_produktion'],
logistik: ['dept_logistik'],
einkauf: ['dept_einkauf'],
facility: ['dept_facility'],
}
/** Mapping department key → scope question ID for Block 9 */
export const DEPT_KEY_TO_QUESTION: Record<string, string> = {
dept_hr: 'dk_dept_hr',
dept_recruiting: 'dk_dept_recruiting',
dept_finance: 'dk_dept_finance',
dept_sales: 'dk_dept_sales',
dept_marketing: 'dk_dept_marketing',
dept_support: 'dk_dept_support',
dept_it: 'dk_dept_it',
dept_recht: 'dk_dept_recht',
dept_produktion: 'dk_dept_produktion',
dept_logistik: 'dk_dept_logistik',
dept_einkauf: 'dk_dept_einkauf',
dept_facility: 'dk_dept_facility',
}
interface DatenkategorienBlock9Props {
answers: ScopeProfilingAnswer[]
onAnswerChange: (questionId: string, value: string | string[] | boolean | number) => void
}
export function DatenkategorienBlock9({
answers,
onAnswerChange,
}: DatenkategorienBlock9Props) {
const [expandedDepts, setExpandedDepts] = useState<Set<string>>(new Set())
const [initializedDepts, setInitializedDepts] = useState<Set<string>>(new Set())
// Get selected departments from Block 8
const deptAnswer = answers.find(a => a.questionId === 'vvt_departments')
const selectedDepts = Array.isArray(deptAnswer?.value) ? (deptAnswer.value as string[]) : []
// Resolve which department keys are active
const activeDeptKeys: string[] = []
for (const deptValue of selectedDepts) {
const keys = DEPT_VALUE_TO_KEY[deptValue]
if (keys) {
for (const k of keys) {
if (!activeDeptKeys.includes(k)) activeDeptKeys.push(k)
}
}
}
const toggleDept = (deptKey: string) => {
setExpandedDepts(prev => {
const next = new Set(prev)
if (next.has(deptKey)) {
next.delete(deptKey)
} else {
next.add(deptKey)
// Prefill typical categories on first expand
if (!initializedDepts.has(deptKey)) {
const config = DEPARTMENT_DATA_CATEGORIES[deptKey]
const questionId = DEPT_KEY_TO_QUESTION[deptKey]
if (config && questionId) {
const existing = answers.find(a => a.questionId === questionId)
if (!existing) {
const typicalIds = config.categories.filter(c => c.isTypical).map(c => c.id)
onAnswerChange(questionId, typicalIds)
}
}
setInitializedDepts(p => new Set(p).add(deptKey))
}
}
return next
})
}
const handleCategoryToggle = (deptKey: string, catId: string) => {
const questionId = DEPT_KEY_TO_QUESTION[deptKey]
if (!questionId) return
const existing = answers.find(a => a.questionId === questionId)
const current = Array.isArray(existing?.value) ? (existing.value as string[]) : []
const updated = current.includes(catId)
? current.filter(id => id !== catId)
: [...current, catId]
onAnswerChange(questionId, updated)
}
if (activeDeptKeys.length === 0) {
return (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-6 text-center">
<p className="text-sm text-yellow-800">
Bitte waehlen Sie zuerst in <strong>Block 8 (Verarbeitungstaetigkeiten)</strong> die
Abteilungen aus, in denen personenbezogene Daten verarbeitet werden.
</p>
</div>
)
}
return (
<div className="space-y-3">
{activeDeptKeys.map(deptKey => {
const config = DEPARTMENT_DATA_CATEGORIES[deptKey]
if (!config) return null
const questionId = DEPT_KEY_TO_QUESTION[deptKey]
const isExpanded = expandedDepts.has(deptKey)
const existing = answers.find(a => a.questionId === questionId)
const selectedCategories = Array.isArray(existing?.value) ? (existing.value as string[]) : []
const hasArt9Selected = config.categories
.filter(c => c.isArt9)
.some(c => selectedCategories.includes(c.id))
return (
<div
key={deptKey}
className={`border rounded-xl overflow-hidden transition-all ${
isExpanded ? 'border-purple-400 bg-white shadow-sm' : 'border-gray-200 bg-white'
}`}
>
{/* Header */}
<button
type="button"
onClick={() => toggleDept(deptKey)}
className="w-full flex items-center justify-between p-4 hover:bg-gray-50 transition-colors"
>
<div className="flex items-center gap-3">
<span className="text-xl">{config.icon}</span>
<div className="text-left">
<span className="text-sm font-medium text-gray-900">{config.label}</span>
{selectedCategories.length > 0 && (
<span className="ml-2 text-xs text-gray-400">
({selectedCategories.length} Kategorien)
</span>
)}
</div>
</div>
<div className="flex items-center gap-2">
{hasArt9Selected && (
<span className="px-1.5 py-0.5 text-[10px] font-semibold bg-orange-100 text-orange-700 rounded">
Art. 9
</span>
)}
<svg
className={`w-5 h-5 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>
{/* Expandable categories panel */}
{isExpanded && (
<div className="border-t border-gray-100 px-4 pt-3 pb-4">
<p className="text-xs font-medium text-gray-500 uppercase tracking-wide mb-3">
Datenkategorien
</p>
<div className="space-y-1.5">
{config.categories.map(cat => {
const isChecked = selectedCategories.includes(cat.id)
return (
<label
key={cat.id}
className={`flex items-start gap-3 p-2.5 rounded-lg cursor-pointer transition-colors ${
cat.isArt9
? isChecked ? 'bg-orange-50 hover:bg-orange-100' : 'bg-gray-50 hover:bg-orange-50'
: isChecked ? 'bg-purple-50 hover:bg-purple-100' : 'bg-gray-50 hover:bg-gray-100'
}`}
>
<input
type="checkbox"
checked={isChecked}
onChange={() => handleCategoryToggle(deptKey, cat.id)}
className={`w-4 h-4 mt-0.5 rounded ${cat.isArt9 ? 'text-orange-500' : 'text-purple-600'}`}
/>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-gray-900">{cat.label}</span>
{cat.isArt9 && (
<span className="px-1.5 py-0.5 text-[10px] font-semibold bg-orange-100 text-orange-700 rounded">
Art. 9
</span>
)}
</div>
<p className="text-xs text-gray-500 mt-0.5">{cat.info}</p>
</div>
</label>
)
})}
</div>
{/* Art. 9 warning */}
{hasArt9Selected && (
<div className="mt-3 p-3 bg-orange-50 border border-orange-200 rounded-lg">
<p className="text-xs text-orange-800">
<span className="font-semibold">Art. 9 DSGVO:</span> Sie verarbeiten besondere Kategorien
personenbezogener Daten. Eine zusaetzliche Rechtsgrundlage nach Art. 9 Abs. 2 DSGVO ist
erforderlich (z.B. § 26 Abs. 3 BDSG fuer Beschaeftigtendaten).
</p>
</div>
)}
</div>
)}
</div>
)
})}
</div>
)
}

View File

@@ -1,10 +1,10 @@
'use client'
import React, { useState, useCallback, useEffect, useMemo } from 'react'
import React, { useState, useCallback, useEffect } from 'react'
import type { ScopeProfilingAnswer, ScopeProfilingQuestion } from '@/lib/sdk/compliance-scope-types'
import { SCOPE_QUESTION_BLOCKS, getBlockProgress, getTotalProgress, getAnswerValue, prefillFromCompanyProfile, getProfileInfoForBlock, getAutoFilledScoringAnswers, getUnansweredRequiredQuestions } from '@/lib/sdk/compliance-scope-profiling'
import { DEPARTMENT_DATA_CATEGORIES } from '@/lib/sdk/vvt-profiling'
import type { ScopeQuestionBlockId } from '@/lib/sdk/compliance-scope-types'
import { useSDK } from '@/lib/sdk'
import { DatenkategorienBlock9 } from './DatenkategorienBlock'
interface ScopeWizardTabProps {
answers: ScopeProfilingAnswer[]
@@ -28,18 +28,15 @@ export function ScopeWizardTab({
const currentBlock = SCOPE_QUESTION_BLOCKS[currentBlockIndex]
const totalProgress = getTotalProgress(answers)
// Load companyProfile from SDK context
const { state: sdkState } = useSDK()
const companyProfile = sdkState.companyProfile
// Track which question IDs were prefilled from profile
const [prefilledIds, setPrefilledIds] = useState<Set<string>>(new Set())
// Auto-prefill from company profile on mount if answers are empty
useEffect(() => {
if (companyProfile && answers.length === 0) {
const prefilled = prefillFromCompanyProfile(companyProfile)
// Also inject auto-filled scoring answers for questions removed from UI
const autoFilled = getAutoFilledScoringAnswers(companyProfile)
const allPrefilled = [...prefilled, ...autoFilled]
if (allPrefilled.length > 0) {
@@ -47,7 +44,6 @@ export function ScopeWizardTab({
setPrefilledIds(new Set(allPrefilled.map(a => a.questionId)))
}
}
// Only run on mount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
@@ -61,7 +57,6 @@ export function ScopeWizardTab({
} else {
onAnswersChange([...answers, { questionId, value }])
}
// Remove from prefilled set when user manually changes
if (prefilledIds.has(questionId)) {
setPrefilledIds(prev => {
const next = new Set(prev)
@@ -78,7 +73,6 @@ export function ScopeWizardTab({
const prefilled = prefillFromCompanyProfile(companyProfile)
const autoFilled = getAutoFilledScoringAnswers(companyProfile)
const allPrefilled = [...prefilled, ...autoFilled]
// Merge with existing answers: prefilled values for questions not yet answered
const existingIds = new Set(answers.map(a => a.questionId))
const newAnswers = [...answers]
const newPrefilledIds = new Set(prefilledIds)
@@ -109,52 +103,25 @@ export function ScopeWizardTab({
const toggleHelp = useCallback((questionId: string) => {
setExpandedHelp(prev => {
const next = new Set(prev)
if (next.has(questionId)) {
next.delete(questionId)
} else {
next.add(questionId)
}
if (next.has(questionId)) { next.delete(questionId) } else { next.add(questionId) }
return next
})
}, [])
// Check if a question was prefilled from company profile
const isPrefilledFromProfile = useCallback((questionId: string) => {
return prefilledIds.has(questionId)
}, [prefilledIds])
const renderHelpText = (question: ScopeProfilingQuestion) => {
if (!question.helpText) return null
return (
<>
<button
type="button"
className="ml-2 text-blue-400 hover:text-blue-600 inline-flex items-center"
onClick={(e) => {
e.preventDefault()
toggleHelp(question.id)
}}
>
<svg className="w-4 h-4" 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 type="button" className="ml-2 text-blue-400 hover:text-blue-600 inline-flex items-center" onClick={(e) => { e.preventDefault(); toggleHelp(question.id) }}>
<svg className="w-4 h-4" 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>
{expandedHelp.has(question.id) && (
<div className="flex items-start gap-2 mt-2 p-2.5 bg-blue-50 rounded-lg text-xs text-blue-700 leading-relaxed">
<svg className="w-4 h-4 mt-0.5 flex-shrink-0 text-blue-500" 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>
<svg className="w-4 h-4 mt-0.5 flex-shrink-0 text-blue-500" 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>
<span>{question.helpText}</span>
</div>
)}
@@ -164,174 +131,74 @@ export function ScopeWizardTab({
const renderPrefilledBadge = (questionId: string) => {
if (!isPrefilledFromProfile(questionId)) return null
return (
<span className="ml-2 inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-700">
Aus Profil
</span>
)
return <span className="ml-2 inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-700">Aus Profil</span>
}
const renderQuestion = (question: ScopeProfilingQuestion) => {
const currentValue = getAnswerValue(answers, question.id)
const label = (
<div className="flex items-center flex-wrap gap-1">
<span className="text-sm font-medium text-gray-900">{question.question}</span>
{question.required && <span className="text-red-500 ml-1">*</span>}
{renderPrefilledBadge(question.id)}
{renderHelpText(question)}
</div>
)
switch (question.type) {
case 'boolean':
return (
<div className="space-y-2">
<div className="flex items-start justify-between">
<div className="flex items-center flex-wrap gap-1">
<span className="text-sm font-medium text-gray-900">
{question.question}
</span>
{question.required && <span className="text-red-500 ml-1">*</span>}
{renderPrefilledBadge(question.id)}
{renderHelpText(question)}
</div>
</div>
<div className="flex items-start justify-between">{label}</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>
<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">
<div className="flex items-center flex-wrap gap-1">
<span className="text-sm font-medium text-gray-900">
{question.question}
</span>
{question.required && <span className="text-red-500 ml-1">*</span>}
{renderPrefilledBadge(question.id)}
{renderHelpText(question)}
</div>
{label}
<div className="space-y-2">
{question.options?.map((option) => (
<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>
<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">
<div className="flex items-center flex-wrap gap-1">
<span className="text-sm font-medium text-gray-900">
{question.question}
</span>
{question.required && <span className="text-red-500 ml-1">*</span>}
{renderPrefilledBadge(question.id)}
{renderHelpText(question)}
</div>
{label}
<div className="space-y-2">
{question.options?.map((option) => {
const selectedValues = Array.isArray(currentValue) ? currentValue as string[] : []
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 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">
<div className="flex items-center flex-wrap gap-1">
<span className="text-sm font-medium text-gray-900">
{question.question}
</span>
{question.required && <span className="text-red-500 ml-1">*</span>}
{renderPrefilledBadge(question.id)}
{renderHelpText(question)}
</div>
<input
type="number"
value={currentValue != null ? String(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"
/>
{label}
<input type="number" value={currentValue != null ? String(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">
<div className="flex items-center flex-wrap gap-1">
<span className="text-sm font-medium text-gray-900">
{question.question}
</span>
{question.required && <span className="text-red-500 ml-1">*</span>}
{renderPrefilledBadge(question.id)}
{renderHelpText(question)}
</div>
<input
type="text"
value={currentValue != null ? String(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"
/>
{label}
<input type="text" value={currentValue != null ? String(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
}
@@ -350,52 +217,27 @@ export function ScopeWizardTab({
const unanswered = getUnansweredRequiredQuestions(answers, block.id)
const hasRequired = block.questions.some(q => q.required)
const allRequiredDone = hasRequired && unanswered.length === 0
// For optional-only blocks: check if any questions were answered
const answeredIds = new Set(answers.map(a => a.questionId))
const hasAnyAnswer = block.questions.some(q => answeredIds.has(q.id))
const optionalDone = !hasRequired && hasAnyAnswer
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'
}`}
>
<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-sm font-medium ${isActive ? 'text-purple-700' : 'text-gray-700'}`}>{block.title}</span>
{allRequiredDone || optionalDone ? (
<span className="flex items-center gap-1 text-xs font-semibold text-green-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.5} d="M5 13l4 4L19 7" />
</svg>
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M5 13l4 4L19 7" /></svg>
{!hasRequired && <span>(optional)</span>}
</span>
) : !hasRequired ? (
<span className="text-xs text-gray-400">(nur optional)</span>
) : (
<span className="text-xs font-semibold text-orange-600">
{unanswered.length} offen
</span>
<span className="text-xs font-semibold text-orange-600">{unanswered.length} offen</span>
)}
</div>
<div className="w-full bg-gray-200 rounded-full h-1.5 overflow-hidden">
<div
className={`h-full transition-all ${
allRequiredDone || optionalDone
? 'bg-green-500'
: !hasRequired
? 'bg-gray-300'
: 'bg-orange-400'
}`}
style={{ width: `${progress}%` }}
/>
<div className={`h-full transition-all ${allRequiredDone || optionalDone ? 'bg-green-500' : !hasRequired ? 'bg-gray-300' : 'bg-orange-400'}`} style={{ width: `${progress}%` }} />
</div>
</button>
)
@@ -411,25 +253,18 @@ export function ScopeWizardTab({
<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">
<span className="text-sm text-gray-500">
{completionStats.answered} / {completionStats.total} Fragen
</span>
<span className="text-sm text-gray-500">{completionStats.answered} / {completionStats.total} Fragen</span>
<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 className="h-full bg-gradient-to-r from-purple-500 to-blue-500 transition-all duration-500" style={{ width: `${totalProgress}%` }} />
</div>
{/* Clickable unanswered required questions summary */}
{/* Unanswered required questions summary */}
{(() => {
const allUnanswered = getUnansweredRequiredQuestions(answers)
if (allUnanswered.length === 0) return null
// Group by block
const byBlock = new Map<string, { blockTitle: string; blockIndex: number; count: number }>()
for (const item of allUnanswered) {
if (!byBlock.has(item.blockId)) {
@@ -438,18 +273,13 @@ export function ScopeWizardTab({
}
byBlock.get(item.blockId)!.count++
}
return (
<div className="mt-3 flex flex-wrap items-center gap-1.5 text-xs">
<span className="text-orange-600 font-medium"> Offene Pflichtfragen:</span>
{Array.from(byBlock.entries()).map(([blockId, info], i) => (
<React.Fragment key={blockId}>
{i > 0 && <span className="text-gray-300">·</span>}
<button
type="button"
onClick={() => setCurrentBlockIndex(info.blockIndex)}
className="text-orange-700 hover:text-orange-900 hover:underline font-medium"
>
<button type="button" onClick={() => setCurrentBlockIndex(info.blockIndex)} className="text-orange-700 hover:text-orange-900 hover:underline font-medium">
{info.blockTitle} ({info.count})
</button>
</React.Fragment>
@@ -467,17 +297,13 @@ export function ScopeWizardTab({
<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"
>
<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 Profil uebernehmen
</button>
)}
</div>
{/* "Aus Profil" Info Box — shown for blocks that have auto-filled data */}
{/* Profile Info Box */}
{companyProfile && (() => {
const profileItems = getProfileInfoForBlock(companyProfile, currentBlock.id as ScopeQuestionBlockId)
if (profileItems.length === 0) return null
@@ -486,27 +312,16 @@ export function ScopeWizardTab({
<div className="flex items-start justify-between">
<div>
<h4 className="text-sm font-medium text-blue-900 mb-2 flex items-center gap-2">
<svg className="w-4 h-4" 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>
<svg className="w-4 h-4" 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>
Aus Unternehmensprofil
</h4>
<div className="flex flex-wrap gap-x-4 gap-y-1 text-sm text-blue-800">
{profileItems.map(item => (
<span key={item.label}>
<span className="font-medium">{item.label}:</span> {item.value}
</span>
))}
{profileItems.map(item => (<span key={item.label}><span className="font-medium">{item.label}:</span> {item.value}</span>))}
</div>
</div>
<a
href="/sdk/company-profile"
className="text-sm text-blue-600 hover:text-blue-800 font-medium whitespace-nowrap flex items-center gap-1"
>
<a href="/sdk/company-profile" className="text-sm text-blue-600 hover:text-blue-800 font-medium whitespace-nowrap flex items-center gap-1">
Profil bearbeiten
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /></svg>
</a>
</div>
</div>
@@ -516,18 +331,11 @@ export function ScopeWizardTab({
{/* Questions */}
<div className="space-y-6">
{currentBlock.id === 'datenkategorien_detail' ? (
<DatenkategorienBlock9
answers={answers}
onAnswerChange={handleAnswerChange}
/>
<DatenkategorienBlock9 answers={answers} onAnswerChange={handleAnswerChange} />
) : (
currentBlock.questions.map((question) => {
const isAnswered = answers.some(a => a.questionId === question.id)
const borderClass = question.required
? isAnswered
? 'border-l-4 border-l-green-400 pl-4'
: 'border-l-4 border-l-orange-400 pl-4'
: ''
const borderClass = question.required ? (isAnswered ? 'border-l-4 border-l-green-400 pl-4' : 'border-l-4 border-l-orange-400 pl-4') : ''
return (
<div key={question.id} className={`border-b border-gray-100 pb-6 last:border-b-0 last:pb-0 ${borderClass}`}>
{renderQuestion(question)}
@@ -540,32 +348,16 @@ export function ScopeWizardTab({
{/* 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"
>
<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">
Zurueck
</button>
<span className="text-sm text-gray-600">
Block {currentBlockIndex + 1} von {SCOPE_QUESTION_BLOCKS.length}
</span>
<span className="text-sm text-gray-600">Block {currentBlockIndex + 1} von {SCOPE_QUESTION_BLOCKS.length}</span>
{currentBlockIndex === SCOPE_QUESTION_BLOCKS.length - 1 ? (
<button
type="button"
onClick={onEvaluate}
disabled={!canEvaluate || isEvaluating}
className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed"
>
<button type="button" onClick={onEvaluate} disabled={!canEvaluate || isEvaluating} className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed">
{isEvaluating ? 'Evaluiere...' : 'Auswertung starten'}
</button>
) : (
<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"
>
<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">
Weiter
</button>
)}
@@ -574,221 +366,3 @@ export function ScopeWizardTab({
</div>
)
}
// =============================================================================
// BLOCK 9: Datenkategorien pro Abteilung (aufklappbare Kacheln)
// =============================================================================
/** Mapping Block 8 vvt_departments values → DEPARTMENT_DATA_CATEGORIES keys */
const DEPT_VALUE_TO_KEY: Record<string, string[]> = {
personal: ['dept_hr', 'dept_recruiting'],
finanzen: ['dept_finance'],
vertrieb: ['dept_sales'],
marketing: ['dept_marketing'],
it: ['dept_it'],
recht: ['dept_recht'],
kundenservice: ['dept_support'],
produktion: ['dept_produktion'],
logistik: ['dept_logistik'],
einkauf: ['dept_einkauf'],
facility: ['dept_facility'],
}
/** Mapping department key → scope question ID for Block 9 */
const DEPT_KEY_TO_QUESTION: Record<string, string> = {
dept_hr: 'dk_dept_hr',
dept_recruiting: 'dk_dept_recruiting',
dept_finance: 'dk_dept_finance',
dept_sales: 'dk_dept_sales',
dept_marketing: 'dk_dept_marketing',
dept_support: 'dk_dept_support',
dept_it: 'dk_dept_it',
dept_recht: 'dk_dept_recht',
dept_produktion: 'dk_dept_produktion',
dept_logistik: 'dk_dept_logistik',
dept_einkauf: 'dk_dept_einkauf',
dept_facility: 'dk_dept_facility',
}
function DatenkategorienBlock9({
answers,
onAnswerChange,
}: {
answers: ScopeProfilingAnswer[]
onAnswerChange: (questionId: string, value: string | string[] | boolean | number) => void
}) {
const [expandedDepts, setExpandedDepts] = useState<Set<string>>(new Set())
const [initializedDepts, setInitializedDepts] = useState<Set<string>>(new Set())
// Get selected departments from Block 8
const deptAnswer = answers.find(a => a.questionId === 'vvt_departments')
const selectedDepts = Array.isArray(deptAnswer?.value) ? (deptAnswer.value as string[]) : []
// Resolve which department keys are active
const activeDeptKeys: string[] = []
for (const deptValue of selectedDepts) {
const keys = DEPT_VALUE_TO_KEY[deptValue]
if (keys) {
for (const k of keys) {
if (!activeDeptKeys.includes(k)) activeDeptKeys.push(k)
}
}
}
const toggleDept = (deptKey: string) => {
setExpandedDepts(prev => {
const next = new Set(prev)
if (next.has(deptKey)) {
next.delete(deptKey)
} else {
next.add(deptKey)
// Prefill typical categories on first expand
if (!initializedDepts.has(deptKey)) {
const config = DEPARTMENT_DATA_CATEGORIES[deptKey]
const questionId = DEPT_KEY_TO_QUESTION[deptKey]
if (config && questionId) {
const existing = answers.find(a => a.questionId === questionId)
if (!existing) {
const typicalIds = config.categories.filter(c => c.isTypical).map(c => c.id)
onAnswerChange(questionId, typicalIds)
}
}
setInitializedDepts(p => new Set(p).add(deptKey))
}
}
return next
})
}
const handleCategoryToggle = (deptKey: string, catId: string) => {
const questionId = DEPT_KEY_TO_QUESTION[deptKey]
if (!questionId) return
const existing = answers.find(a => a.questionId === questionId)
const current = Array.isArray(existing?.value) ? (existing.value as string[]) : []
const updated = current.includes(catId)
? current.filter(id => id !== catId)
: [...current, catId]
onAnswerChange(questionId, updated)
}
if (activeDeptKeys.length === 0) {
return (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-6 text-center">
<p className="text-sm text-yellow-800">
Bitte waehlen Sie zuerst in <strong>Block 8 (Verarbeitungstaetigkeiten)</strong> die
Abteilungen aus, in denen personenbezogene Daten verarbeitet werden.
</p>
</div>
)
}
return (
<div className="space-y-3">
{activeDeptKeys.map(deptKey => {
const config = DEPARTMENT_DATA_CATEGORIES[deptKey]
if (!config) return null
const questionId = DEPT_KEY_TO_QUESTION[deptKey]
const isExpanded = expandedDepts.has(deptKey)
const existing = answers.find(a => a.questionId === questionId)
const selectedCategories = Array.isArray(existing?.value) ? (existing.value as string[]) : []
const hasArt9Selected = config.categories
.filter(c => c.isArt9)
.some(c => selectedCategories.includes(c.id))
return (
<div
key={deptKey}
className={`border rounded-xl overflow-hidden transition-all ${
isExpanded ? 'border-purple-400 bg-white shadow-sm' : 'border-gray-200 bg-white'
}`}
>
{/* Header */}
<button
type="button"
onClick={() => toggleDept(deptKey)}
className="w-full flex items-center justify-between p-4 hover:bg-gray-50 transition-colors"
>
<div className="flex items-center gap-3">
<span className="text-xl">{config.icon}</span>
<div className="text-left">
<span className="text-sm font-medium text-gray-900">{config.label}</span>
{selectedCategories.length > 0 && (
<span className="ml-2 text-xs text-gray-400">
({selectedCategories.length} Kategorien)
</span>
)}
</div>
</div>
<div className="flex items-center gap-2">
{hasArt9Selected && (
<span className="px-1.5 py-0.5 text-[10px] font-semibold bg-orange-100 text-orange-700 rounded">
Art. 9
</span>
)}
<svg
className={`w-5 h-5 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>
{/* Expandable categories panel */}
{isExpanded && (
<div className="border-t border-gray-100 px-4 pt-3 pb-4">
<p className="text-xs font-medium text-gray-500 uppercase tracking-wide mb-3">
Datenkategorien
</p>
<div className="space-y-1.5">
{config.categories.map(cat => {
const isChecked = selectedCategories.includes(cat.id)
return (
<label
key={cat.id}
className={`flex items-start gap-3 p-2.5 rounded-lg cursor-pointer transition-colors ${
cat.isArt9
? isChecked ? 'bg-orange-50 hover:bg-orange-100' : 'bg-gray-50 hover:bg-orange-50'
: isChecked ? 'bg-purple-50 hover:bg-purple-100' : 'bg-gray-50 hover:bg-gray-100'
}`}
>
<input
type="checkbox"
checked={isChecked}
onChange={() => handleCategoryToggle(deptKey, cat.id)}
className={`w-4 h-4 mt-0.5 rounded ${cat.isArt9 ? 'text-orange-500' : 'text-purple-600'}`}
/>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-gray-900">{cat.label}</span>
{cat.isArt9 && (
<span className="px-1.5 py-0.5 text-[10px] font-semibold bg-orange-100 text-orange-700 rounded">
Art. 9
</span>
)}
</div>
<p className="text-xs text-gray-500 mt-0.5">{cat.info}</p>
</div>
</label>
)
})}
</div>
{/* Art. 9 warning */}
{hasArt9Selected && (
<div className="mt-3 p-3 bg-orange-50 border border-orange-200 rounded-lg">
<p className="text-xs text-orange-800">
<span className="font-semibold">Art. 9 DSGVO:</span> Sie verarbeiten besondere Kategorien
personenbezogener Daten. Eine zusaetzliche Rechtsgrundlage nach Art. 9 Abs. 2 DSGVO ist
erforderlich (z.B. § 26 Abs. 3 BDSG fuer Beschaeftigtendaten).
</p>
</div>
)}
</div>
)}
</div>
)
})}
</div>
)
}

View File

@@ -0,0 +1,222 @@
'use client'
// =============================================================================
// PII RULE MODALS
// NewRuleModal and EditRuleModal extracted from PIIRulesTab for LOC compliance.
// =============================================================================
interface PIIRule {
id: string
name: string
description?: string
pattern?: string
category: string
action: string
active: boolean
created_at: string
}
export const CATEGORIES = [
{ value: 'email', label: 'E-Mail-Adressen' },
{ value: 'phone', label: 'Telefonnummern' },
{ value: 'iban', label: 'IBAN/Bankdaten' },
{ value: 'name', label: 'Personennamen' },
{ value: 'address', label: 'Adressen' },
{ value: 'id_number', label: 'Ausweisnummern' },
{ value: 'health', label: 'Gesundheitsdaten' },
{ value: 'other', label: 'Sonstige' },
]
export const ACTIONS = [
{ value: 'warn', label: 'Warnung', color: 'bg-amber-100 text-amber-700' },
{ value: 'mask', label: 'Maskieren', color: 'bg-orange-100 text-orange-700' },
{ value: 'block', label: 'Blockieren', color: 'bg-red-100 text-red-700' },
]
// ---------------------------------------------------------------------------
// NewRuleModal
// ---------------------------------------------------------------------------
export interface NewRuleState {
name: string
pattern: string
category: string
action: string
active: boolean
}
interface NewRuleModalProps {
newRule: NewRuleState
onChange: (rule: NewRuleState) => void
onSubmit: () => void
onClose: () => void
saving: boolean
}
export function NewRuleModal({ newRule, onChange, onSubmit, onClose, saving }: NewRuleModalProps) {
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-xl p-6 w-full max-w-lg mx-4">
<h3 className="text-lg font-semibold text-slate-900 mb-4">Neue PII-Regel</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Name *</label>
<input
type="text"
value={newRule.name}
onChange={(e) => onChange({ ...newRule, name: e.target.value })}
placeholder="z.B. Deutsche Telefonnummern"
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Kategorie *</label>
<select
value={newRule.category}
onChange={(e) => onChange({ ...newRule, category: e.target.value })}
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
>
{CATEGORIES.map((c) => (
<option key={c.value} value={c.value}>{c.label}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Muster (Regex)</label>
<textarea
value={newRule.pattern}
onChange={(e) => onChange({ ...newRule, pattern: e.target.value })}
placeholder={'Regex-Muster, z.B. (?:\\+49|0)[\\s.-]?\\d{2,4}...'}
rows={3}
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent font-mono text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Aktion *</label>
<select
value={newRule.action}
onChange={(e) => onChange({ ...newRule, action: e.target.value })}
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
>
{ACTIONS.map((a) => (
<option key={a.value} value={a.value}>{a.label}</option>
))}
</select>
</div>
</div>
<div className="flex justify-end gap-3 mt-6 pt-4 border-t border-slate-100">
<button onClick={onClose} className="px-4 py-2 text-slate-600 hover:text-slate-700">
Abbrechen
</button>
<button
onClick={onSubmit}
disabled={saving || !newRule.name || !newRule.pattern}
className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50"
>
{saving ? 'Speichere...' : 'Erstellen'}
</button>
</div>
</div>
</div>
)
}
// ---------------------------------------------------------------------------
// EditRuleModal
// ---------------------------------------------------------------------------
interface EditRuleModalProps {
editingRule: PIIRule
onChange: (rule: PIIRule) => void
onSubmit: () => void
onClose: () => void
saving: boolean
}
export function EditRuleModal({ editingRule, onChange, onSubmit, onClose, saving }: EditRuleModalProps) {
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-xl p-6 w-full max-w-lg mx-4">
<h3 className="text-lg font-semibold text-slate-900 mb-4">PII-Regel bearbeiten</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Name *</label>
<input
type="text"
value={editingRule.name}
onChange={(e) => onChange({ ...editingRule, name: e.target.value })}
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Kategorie</label>
<select
value={editingRule.category}
onChange={(e) => onChange({ ...editingRule, category: e.target.value })}
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
>
{CATEGORIES.map((c) => (
<option key={c.value} value={c.value}>{c.label}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Muster (Regex)</label>
<textarea
value={editingRule.pattern || ''}
onChange={(e) => onChange({ ...editingRule, pattern: e.target.value })}
rows={3}
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent font-mono text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Aktion</label>
<select
value={editingRule.action}
onChange={(e) => onChange({ ...editingRule, action: e.target.value })}
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
>
{ACTIONS.map((a) => (
<option key={a.value} value={a.value}>{a.label}</option>
))}
</select>
</div>
<div className="flex items-center gap-2">
<input
type="checkbox"
id="edit_active"
checked={editingRule.active}
onChange={(e) => onChange({ ...editingRule, active: e.target.checked })}
className="w-4 h-4 text-purple-600"
/>
<label htmlFor="edit_active" className="text-sm text-slate-700">
Aktiv
</label>
</div>
</div>
<div className="flex justify-end gap-3 mt-6 pt-4 border-t border-slate-100">
<button onClick={onClose} className="px-4 py-2 text-slate-600 hover:text-slate-700">
Abbrechen
</button>
<button
onClick={onSubmit}
disabled={saving}
className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50"
>
{saving ? 'Speichere...' : 'Speichern'}
</button>
</div>
</div>
</div>
)
}

View File

@@ -1,6 +1,7 @@
'use client'
import { useState, useEffect } from 'react'
import { NewRuleModal, EditRuleModal, CATEGORIES, ACTIONS, type NewRuleState } from './PIIRuleModals'
interface PIIRule {
id: string
@@ -34,53 +35,22 @@ interface PIIRulesTabProps {
onUpdate?: () => void
}
const CATEGORIES = [
{ value: 'email', label: 'E-Mail-Adressen' },
{ value: 'phone', label: 'Telefonnummern' },
{ value: 'iban', label: 'IBAN/Bankdaten' },
{ value: 'name', label: 'Personennamen' },
{ value: 'address', label: 'Adressen' },
{ value: 'id_number', label: 'Ausweisnummern' },
{ value: 'health', label: 'Gesundheitsdaten' },
{ value: 'other', label: 'Sonstige' },
]
const ACTIONS = [
{ value: 'warn', label: 'Warnung', color: 'bg-amber-100 text-amber-700' },
{ value: 'mask', label: 'Maskieren', color: 'bg-orange-100 text-orange-700' },
{ value: 'block', label: 'Blockieren', color: 'bg-red-100 text-red-700' },
]
export function PIIRulesTab({ apiBase, onUpdate }: PIIRulesTabProps) {
const [rules, setRules] = useState<PIIRule[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
// Category filter
const [categoryFilter, setCategoryFilter] = useState('')
// Test panel
const [testText, setTestText] = useState('')
const [testResult, setTestResult] = useState<PIITestResult | null>(null)
const [testing, setTesting] = useState(false)
// Edit modal
const [editingRule, setEditingRule] = useState<PIIRule | null>(null)
const [isNewRule, setIsNewRule] = useState(false)
const [saving, setSaving] = useState(false)
// New rule form
const [newRule, setNewRule] = useState({
name: '',
pattern: '',
category: 'email',
action: 'block',
active: true,
const [newRule, setNewRule] = useState<NewRuleState>({
name: '', pattern: '', category: 'email', action: 'block', active: true,
})
useEffect(() => {
fetchRules()
}, [categoryFilter])
useEffect(() => { fetchRules() }, [categoryFilter]) // eslint-disable-line react-hooks/exhaustive-deps
const fetchRules = async () => {
try {
@@ -89,7 +59,6 @@ export function PIIRulesTab({ apiBase, onUpdate }: PIIRulesTabProps) {
if (categoryFilter) params.append('category', categoryFilter)
const res = await fetch(`${apiBase}/pii-rules?${params}`)
if (!res.ok) throw new Error('Fehler beim Laden')
const data = await res.json()
setRules(data.rules || [])
} catch (err) {
@@ -107,16 +76,8 @@ export function PIIRulesTab({ apiBase, onUpdate }: PIIRulesTabProps) {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newRule),
})
if (!res.ok) throw new Error('Fehler beim Erstellen')
setNewRule({
name: '',
pattern: '',
category: 'email',
action: 'block',
active: true,
})
setNewRule({ name: '', pattern: '', category: 'email', action: 'block', active: true })
setIsNewRule(false)
fetchRules()
onUpdate?.()
@@ -129,7 +90,6 @@ export function PIIRulesTab({ apiBase, onUpdate }: PIIRulesTabProps) {
const updateRule = async () => {
if (!editingRule) return
try {
setSaving(true)
const res = await fetch(`${apiBase}/pii-rules/${editingRule.id}`, {
@@ -137,9 +97,7 @@ export function PIIRulesTab({ apiBase, onUpdate }: PIIRulesTabProps) {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(editingRule),
})
if (!res.ok) throw new Error('Fehler beim Aktualisieren')
setEditingRule(null)
fetchRules()
onUpdate?.()
@@ -152,14 +110,9 @@ export function PIIRulesTab({ apiBase, onUpdate }: PIIRulesTabProps) {
const deleteRule = async (id: string) => {
if (!confirm('Regel wirklich loeschen? Diese Aktion wird im Audit-Log protokolliert.')) return
try {
const res = await fetch(`${apiBase}/pii-rules/${id}`, {
method: 'DELETE',
})
const res = await fetch(`${apiBase}/pii-rules/${id}`, { method: 'DELETE' })
if (!res.ok) throw new Error('Fehler beim Loeschen')
fetchRules()
onUpdate?.()
} catch (err) {
@@ -174,9 +127,7 @@ export function PIIRulesTab({ apiBase, onUpdate }: PIIRulesTabProps) {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ active: !rule.active }),
})
if (!res.ok) throw new Error('Fehler beim Aendern des Status')
fetchRules()
onUpdate?.()
} catch (err) {
@@ -186,47 +137,25 @@ export function PIIRulesTab({ apiBase, onUpdate }: PIIRulesTabProps) {
const runTest = () => {
if (!testText) return
setTesting(true)
const matches: PIIMatch[] = []
const activeRules = rules.filter((r) => r.active && r.pattern)
for (const rule of activeRules) {
try {
const regex = new RegExp(rule.pattern!, 'gi')
let m: RegExpExecArray | null
while ((m = regex.exec(testText)) !== null) {
matches.push({
rule_id: rule.id,
rule_name: rule.name,
category: rule.category,
action: rule.action,
match: m[0],
start_index: m.index,
end_index: m.index + m[0].length,
})
matches.push({ rule_id: rule.id, rule_name: rule.name, category: rule.category, action: rule.action, match: m[0], start_index: m.index, end_index: m.index + m[0].length })
}
} catch {
// Invalid regex — skip
}
} catch { /* Invalid regex — skip */ }
}
const shouldBlock = matches.some((m) => m.action === 'block')
setTestResult({
has_pii: matches.length > 0,
matches,
should_block: shouldBlock,
})
setTestResult({ has_pii: matches.length > 0, matches, should_block: matches.some(m => m.action === 'block') })
setTesting(false)
}
const getActionBadge = (action: string) => {
const config = ACTIONS.find((a) => a.value === action)
return (
<span className={`px-2 py-1 rounded text-xs font-medium ${config?.color || 'bg-slate-100 text-slate-700'}`}>
{config?.label || action}
</span>
)
return <span className={`px-2 py-1 rounded text-xs font-medium ${config?.color || 'bg-slate-100 text-slate-700'}`}>{config?.label || action}</span>
}
return (
@@ -235,88 +164,38 @@ export function PIIRulesTab({ apiBase, onUpdate }: PIIRulesTabProps) {
{error && (
<div className="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg text-red-700 flex items-center justify-between">
<span>{error}</span>
<button onClick={() => setError(null)} className="text-red-500 hover:text-red-700">
&times;
</button>
<button onClick={() => setError(null)} className="text-red-500 hover:text-red-700">&times;</button>
</div>
)}
{/* Test Panel */}
<div className="bg-white rounded-xl border border-slate-200 p-6 mb-6">
<h3 className="font-semibold text-slate-900 mb-4">PII-Test</h3>
<p className="text-sm text-slate-600 mb-4">
Testen Sie, ob ein Text personenbezogene Daten (PII) enthaelt.
</p>
<textarea
value={testText}
onChange={(e) => setTestText(e.target.value)}
placeholder="Geben Sie hier einen Text zum Testen ein...
Beispiel:
Kontaktieren Sie mich unter max.mustermann@example.com oder
rufen Sie mich an unter +49 170 1234567.
Meine IBAN ist DE89 3704 0044 0532 0130 00."
rows={6}
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent font-mono text-sm"
/>
<p className="text-sm text-slate-600 mb-4">Testen Sie, ob ein Text personenbezogene Daten (PII) enthaelt.</p>
<textarea value={testText} onChange={(e) => setTestText(e.target.value)} placeholder={"Geben Sie hier einen Text zum Testen ein...\n\nBeispiel:\nKontaktieren Sie mich unter max.mustermann@example.com oder\nrufen Sie mich an unter +49 170 1234567.\nMeine IBAN ist DE89 3704 0044 0532 0130 00."} rows={6} className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent font-mono text-sm" />
<div className="flex justify-between items-center mt-4">
<button
onClick={() => {
setTestText('')
setTestResult(null)
}}
className="text-sm text-slate-500 hover:text-slate-700"
>
Zuruecksetzen
</button>
<button
onClick={runTest}
disabled={testing || !testText}
className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50"
>
{testing ? 'Teste...' : 'Testen'}
</button>
<button onClick={() => { setTestText(''); setTestResult(null) }} className="text-sm text-slate-500 hover:text-slate-700">Zuruecksetzen</button>
<button onClick={runTest} disabled={testing || !testText} className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50">{testing ? 'Teste...' : 'Testen'}</button>
</div>
{/* Test Results */}
{testResult && (
<div className={`mt-4 p-4 rounded-lg ${testResult.should_block ? 'bg-red-50 border border-red-200' : testResult.has_pii ? 'bg-amber-50 border border-amber-200' : 'bg-green-50 border border-green-200'}`}>
<div className="flex items-center gap-2 mb-3">
{testResult.should_block ? (
<>
<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-red-800">Blockiert - Kritische PII gefunden</span>
</>
<><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-red-800">Blockiert - Kritische PII gefunden</span></>
) : testResult.has_pii ? (
<>
<svg className="w-5 h-5 text-amber-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-amber-800">Warnung - PII gefunden ({testResult.matches.length} Treffer)</span>
</>
<><svg className="w-5 h-5 text-amber-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-amber-800">Warnung - PII gefunden ({testResult.matches.length} Treffer)</span></>
) : (
<>
<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="M5 13l4 4L19 7" />
</svg>
<span className="font-medium text-green-800">Keine PII gefunden</span>
</>
<><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="M5 13l4 4L19 7" /></svg><span className="font-medium text-green-800">Keine PII gefunden</span></>
)}
</div>
{testResult.matches.length > 0 && (
<div className="space-y-2">
{testResult.matches.map((match, idx) => (
<div key={idx} className="flex items-center gap-3 text-sm bg-white bg-opacity-50 rounded px-3 py-2">
{getActionBadge(match.action)}
<span className="text-slate-700 font-medium">{match.rule_name}</span>
<code className="text-xs bg-slate-100 px-2 py-1 rounded">
{match.match.length > 30 ? match.match.substring(0, 30) + '...' : match.match}
</code>
<code className="text-xs bg-slate-100 px-2 py-1 rounded">{match.match.length > 30 ? match.match.substring(0, 30) + '...' : match.match}</code>
</div>
))}
</div>
@@ -329,23 +208,12 @@ Meine IBAN ist DE89 3704 0044 0532 0130 00."
<div className="flex flex-wrap justify-between items-center gap-3 mb-4">
<h3 className="font-semibold text-slate-900">PII-Erkennungsregeln</h3>
<div className="flex gap-3 items-center">
<select
value={categoryFilter}
onChange={(e) => setCategoryFilter(e.target.value)}
className="px-3 py-2 border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent"
>
<select value={categoryFilter} onChange={(e) => setCategoryFilter(e.target.value)} className="px-3 py-2 border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent">
<option value="">Alle Kategorien</option>
{CATEGORIES.map((c) => (
<option key={c.value} value={c.value}>{c.label}</option>
))}
{CATEGORIES.map((c) => (<option key={c.value} value={c.value}>{c.label}</option>))}
</select>
<button
onClick={() => setIsNewRule(true)}
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 flex items-center gap-2"
>
<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>
<button onClick={() => setIsNewRule(true)} className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 flex items-center gap-2">
<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>
Neue Regel
</button>
</div>
@@ -356,13 +224,9 @@ Meine IBAN ist DE89 3704 0044 0532 0130 00."
<div className="text-center py-12 text-slate-500">Lade Regeln...</div>
) : rules.length === 0 ? (
<div className="bg-white rounded-xl border border-slate-200 p-8 text-center">
<svg className="w-12 h-12 mx-auto text-slate-300 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
<svg className="w-12 h-12 mx-auto text-slate-300 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" /></svg>
<h3 className="text-lg font-medium text-slate-700 mb-2">Keine Regeln vorhanden</h3>
<p className="text-sm text-slate-500">
Fuegen Sie PII-Erkennungsregeln hinzu.
</p>
<p className="text-sm text-slate-500">Fuegen Sie PII-Erkennungsregeln hinzu.</p>
</div>
) : (
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
@@ -381,42 +245,17 @@ Meine IBAN ist DE89 3704 0044 0532 0130 00."
{rules.map((rule) => (
<tr key={rule.id} className="hover:bg-slate-50">
<td className="px-4 py-3 text-sm font-medium text-slate-800">{rule.name}</td>
<td className="px-4 py-3">
<span className="text-xs bg-slate-100 text-slate-700 px-2 py-1 rounded">
{CATEGORIES.find((c) => c.value === rule.category)?.label || rule.category}
</span>
</td>
<td className="px-4 py-3">
<code className="text-xs bg-slate-100 px-2 py-1 rounded max-w-xs truncate block">
{rule.pattern && rule.pattern.length > 40 ? rule.pattern.substring(0, 40) + '...' : rule.pattern || '-'}
</code>
</td>
<td className="px-4 py-3"><span className="text-xs bg-slate-100 text-slate-700 px-2 py-1 rounded">{CATEGORIES.find((c) => c.value === rule.category)?.label || rule.category}</span></td>
<td className="px-4 py-3"><code className="text-xs bg-slate-100 px-2 py-1 rounded max-w-xs truncate block">{rule.pattern && rule.pattern.length > 40 ? rule.pattern.substring(0, 40) + '...' : rule.pattern || '-'}</code></td>
<td className="px-4 py-3">{getActionBadge(rule.action)}</td>
<td className="px-4 py-3">
<button
onClick={() => toggleRuleStatus(rule)}
className={`text-xs px-2 py-1 rounded ${
rule.active
? 'bg-green-100 text-green-700'
: 'bg-red-100 text-red-700'
}`}
>
<button onClick={() => toggleRuleStatus(rule)} className={`text-xs px-2 py-1 rounded ${rule.active ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'}`}>
{rule.active ? 'Aktiv' : 'Inaktiv'}
</button>
</td>
<td className="px-4 py-3 text-right">
<button
onClick={() => setEditingRule(rule)}
className="text-purple-600 hover:text-purple-700 mr-3"
>
Bearbeiten
</button>
<button
onClick={() => deleteRule(rule.id)}
className="text-red-600 hover:text-red-700"
>
Loeschen
</button>
<button onClick={() => setEditingRule(rule)} className="text-purple-600 hover:text-purple-700 mr-3">Bearbeiten</button>
<button onClick={() => deleteRule(rule.id)} className="text-red-600 hover:text-red-700">Loeschen</button>
</td>
</tr>
))}
@@ -425,173 +264,25 @@ Meine IBAN ist DE89 3704 0044 0532 0130 00."
</div>
)}
{/* New Rule Modal */}
{/* Modals */}
{isNewRule && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-xl p-6 w-full max-w-lg mx-4">
<h3 className="text-lg font-semibold text-slate-900 mb-4">Neue PII-Regel</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Name *</label>
<input
type="text"
value={newRule.name}
onChange={(e) => setNewRule({ ...newRule, name: e.target.value })}
placeholder="z.B. Deutsche Telefonnummern"
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Kategorie *</label>
<select
value={newRule.category}
onChange={(e) => setNewRule({ ...newRule, category: e.target.value })}
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
>
{CATEGORIES.map((c) => (
<option key={c.value} value={c.value}>
{c.label}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Muster (Regex)</label>
<textarea
value={newRule.pattern}
onChange={(e) => setNewRule({ ...newRule, pattern: e.target.value })}
placeholder={'Regex-Muster, z.B. (?:\\+49|0)[\\s.-]?\\d{2,4}...'}
rows={3}
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent font-mono text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Aktion *</label>
<select
value={newRule.action}
onChange={(e) => setNewRule({ ...newRule, action: e.target.value })}
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
>
{ACTIONS.map((a) => (
<option key={a.value} value={a.value}>
{a.label}
</option>
))}
</select>
</div>
</div>
<div className="flex justify-end gap-3 mt-6 pt-4 border-t border-slate-100">
<button
onClick={() => setIsNewRule(false)}
className="px-4 py-2 text-slate-600 hover:text-slate-700"
>
Abbrechen
</button>
<button
onClick={createRule}
disabled={saving || !newRule.name || !newRule.pattern}
className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50"
>
{saving ? 'Speichere...' : 'Erstellen'}
</button>
</div>
</div>
</div>
<NewRuleModal
newRule={newRule}
onChange={setNewRule}
onSubmit={createRule}
onClose={() => setIsNewRule(false)}
saving={saving}
/>
)}
{/* Edit Rule Modal */}
{editingRule && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-xl p-6 w-full max-w-lg mx-4">
<h3 className="text-lg font-semibold text-slate-900 mb-4">PII-Regel bearbeiten</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Name *</label>
<input
type="text"
value={editingRule.name}
onChange={(e) => setEditingRule({ ...editingRule, name: e.target.value })}
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Kategorie</label>
<select
value={editingRule.category}
onChange={(e) => setEditingRule({ ...editingRule, category: e.target.value })}
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
>
{CATEGORIES.map((c) => (
<option key={c.value} value={c.value}>
{c.label}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Muster (Regex)</label>
<textarea
value={editingRule.pattern || ''}
onChange={(e) => setEditingRule({ ...editingRule, pattern: e.target.value })}
rows={3}
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent font-mono text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Aktion</label>
<select
value={editingRule.action}
onChange={(e) => setEditingRule({ ...editingRule, action: e.target.value })}
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
>
{ACTIONS.map((a) => (
<option key={a.value} value={a.value}>
{a.label}
</option>
))}
</select>
</div>
<div className="flex items-center gap-2">
<input
type="checkbox"
id="edit_active"
checked={editingRule.active}
onChange={(e) => setEditingRule({ ...editingRule, active: e.target.checked })}
className="w-4 h-4 text-purple-600"
/>
<label htmlFor="edit_active" className="text-sm text-slate-700">
Aktiv
</label>
</div>
</div>
<div className="flex justify-end gap-3 mt-6 pt-4 border-t border-slate-100">
<button
onClick={() => setEditingRule(null)}
className="px-4 py-2 text-slate-600 hover:text-slate-700"
>
Abbrechen
</button>
<button
onClick={updateRule}
disabled={saving}
className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50"
>
{saving ? 'Speichere...' : 'Speichern'}
</button>
</div>
</div>
</div>
<EditRuleModal
editingRule={editingRule}
onChange={setEditingRule}
onSubmit={updateRule}
onClose={() => setEditingRule(null)}
saving={saving}
/>
)}
</div>
)

View File

@@ -0,0 +1,427 @@
'use client'
// =============================================================================
// ReviewExportPanels
// SummaryCard, TOMsTable, GapAnalysisPanel, ExportPanel extracted from
// ReviewExportStep for LOC compliance.
// =============================================================================
import { useEffect, useState } from 'react'
import { useTOMGenerator } from '@/lib/sdk/tom-generator'
import { CONTROL_CATEGORIES } from '@/lib/sdk/tom-generator/types'
import { getControlById } from '@/lib/sdk/tom-generator/controls/loader'
import { generateDOCXBlob } from '@/lib/sdk/tom-generator/export/docx'
import { generatePDFBlob } from '@/lib/sdk/tom-generator/export/pdf'
import { generateZIPBlob } from '@/lib/sdk/tom-generator/export/zip'
// =============================================================================
// SUMMARY CARD COMPONENT
// =============================================================================
interface SummaryCardProps {
title: string
value: string | number
description?: string
variant?: 'default' | 'success' | 'warning' | 'danger'
}
export function SummaryCard({ title, value, description, variant = 'default' }: SummaryCardProps) {
const colors = {
default: 'bg-gray-100 text-gray-800',
success: 'bg-green-100 text-green-800',
warning: 'bg-yellow-100 text-yellow-800',
danger: 'bg-red-100 text-red-800',
}
return (
<div className={`rounded-lg p-4 ${colors[variant]}`}>
<div className="text-2xl font-bold">{value}</div>
<div className="font-medium">{title}</div>
{description && <div className="text-sm opacity-75 mt-1">{description}</div>}
</div>
)
}
// =============================================================================
// TOMS TABLE COMPONENT
// =============================================================================
export function TOMsTable() {
const { state } = useTOMGenerator()
const [selectedCategory, setSelectedCategory] = useState<string>('all')
const [selectedApplicability, setSelectedApplicability] = useState<string>('all')
const filteredTOMs = state.derivedTOMs.filter((tom) => {
const control = getControlById(tom.controlId)
const categoryMatch = selectedCategory === 'all' || control?.category === selectedCategory
const applicabilityMatch = selectedApplicability === 'all' || tom.applicability === selectedApplicability
return categoryMatch && applicabilityMatch
})
const getStatusBadge = (status: string) => {
const badges: Record<string, { bg: string; text: string }> = {
IMPLEMENTED: { bg: 'bg-green-100', text: 'text-green-800' },
PARTIAL: { bg: 'bg-yellow-100', text: 'text-yellow-800' },
NOT_IMPLEMENTED: { bg: 'bg-red-100', text: 'text-red-800' },
}
const labels: Record<string, string> = {
IMPLEMENTED: 'Umgesetzt',
PARTIAL: 'Teilweise',
NOT_IMPLEMENTED: 'Offen',
}
const config = badges[status] || badges.NOT_IMPLEMENTED
return (
<span className={`px-2 py-1 rounded-full text-xs font-medium ${config.bg} ${config.text}`}>
{labels[status] || status}
</span>
)
}
const getApplicabilityBadge = (applicability: string) => {
const badges: Record<string, { bg: string; text: string }> = {
REQUIRED: { bg: 'bg-red-100', text: 'text-red-800' },
RECOMMENDED: { bg: 'bg-blue-100', text: 'text-blue-800' },
OPTIONAL: { bg: 'bg-gray-100', text: 'text-gray-800' },
NOT_APPLICABLE: { bg: 'bg-gray-50', text: 'text-gray-500' },
}
const labels: Record<string, string> = {
REQUIRED: 'Erforderlich',
RECOMMENDED: 'Empfohlen',
OPTIONAL: 'Optional',
NOT_APPLICABLE: 'N/A',
}
const config = badges[applicability] || badges.OPTIONAL
return (
<span className={`px-2 py-1 rounded-full text-xs font-medium ${config.bg} ${config.text}`}>
{labels[applicability] || applicability}
</span>
)
}
return (
<div>
{/* Filters */}
<div className="flex flex-wrap gap-4 mb-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Kategorie</label>
<select
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
>
<option value="all">Alle Kategorien</option>
{CONTROL_CATEGORIES.map((cat) => (
<option key={cat.id} value={cat.id}>{cat.name.de}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Anwendbarkeit</label>
<select
value={selectedApplicability}
onChange={(e) => setSelectedApplicability(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
>
<option value="all">Alle</option>
<option value="REQUIRED">Erforderlich</option>
<option value="RECOMMENDED">Empfohlen</option>
<option value="OPTIONAL">Optional</option>
<option value="NOT_APPLICABLE">Nicht anwendbar</option>
</select>
</div>
</div>
{/* Table */}
<div className="overflow-x-auto border rounded-lg">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
ID
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Maßnahme
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Anwendbarkeit
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Nachweise
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{filteredTOMs.map((tom) => (
<tr key={tom.id} className="hover:bg-gray-50">
<td className="px-4 py-3 text-sm font-mono text-gray-900">
{tom.controlId}
</td>
<td className="px-4 py-3">
<div className="text-sm font-medium text-gray-900">{tom.name}</div>
<div className="text-xs text-gray-500 max-w-md truncate">{tom.applicabilityReason}</div>
</td>
<td className="px-4 py-3">
{getApplicabilityBadge(tom.applicability)}
</td>
<td className="px-4 py-3">
{getStatusBadge(tom.implementationStatus)}
</td>
<td className="px-4 py-3 text-sm text-gray-500">
{tom.linkedEvidence.length > 0 ? (
<span className="text-green-600">{tom.linkedEvidence.length} Dok.</span>
) : tom.evidenceGaps.length > 0 ? (
<span className="text-red-600">{tom.evidenceGaps.length} fehlen</span>
) : (
'-'
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="mt-2 text-sm text-gray-500">
{filteredTOMs.length} von {state.derivedTOMs.length} Maßnahmen angezeigt
</div>
</div>
)
}
// =============================================================================
// GAP ANALYSIS PANEL
// =============================================================================
export function GapAnalysisPanel() {
const { state, runGapAnalysis } = useTOMGenerator()
useEffect(() => {
if (!state.gapAnalysis && state.derivedTOMs.length > 0) {
runGapAnalysis()
}
}, [state.derivedTOMs, state.gapAnalysis, runGapAnalysis])
if (!state.gapAnalysis) {
return (
<div className="text-center py-8 text-gray-500">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-2" />
Lückenanalyse wird durchgeführt...
</div>
)
}
const { overallScore, missingControls, partialControls, recommendations } = state.gapAnalysis
const getScoreColor = (score: number) => {
if (score >= 80) return 'text-green-600'
if (score >= 50) return 'text-yellow-600'
return 'text-red-600'
}
return (
<div className="space-y-6">
{/* Score */}
<div className="text-center">
<div className={`text-5xl font-bold ${getScoreColor(overallScore)}`}>
{overallScore}%
</div>
<div className="text-gray-600 mt-1">Compliance Score</div>
</div>
{/* Progress bar */}
<div className="h-4 bg-gray-200 rounded-full overflow-hidden">
<div
className={`h-full transition-all duration-500 ${
overallScore >= 80 ? 'bg-green-500' : overallScore >= 50 ? 'bg-yellow-500' : 'bg-red-500'
}`}
style={{ width: `${overallScore}%` }}
/>
</div>
{/* Missing Controls */}
{missingControls.length > 0 && (
<div>
<h4 className="font-medium text-gray-900 mb-2">
Fehlende Maßnahmen ({missingControls.length})
</h4>
<div className="space-y-2 max-h-48 overflow-y-auto">
{missingControls.map((mc) => {
const control = getControlById(mc.controlId)
return (
<div key={mc.controlId} className="flex items-center justify-between p-2 bg-red-50 rounded-lg">
<div>
<span className="font-mono text-sm text-gray-600">{mc.controlId}</span>
<span className="ml-2 text-sm text-gray-900">{control?.name.de}</span>
</div>
<span className={`px-2 py-0.5 text-xs rounded ${
mc.priority === 'CRITICAL' ? 'bg-red-200 text-red-800' :
mc.priority === 'HIGH' ? 'bg-orange-200 text-orange-800' :
'bg-gray-200 text-gray-800'
}`}>
{mc.priority}
</span>
</div>
)
})}
</div>
</div>
)}
{/* Partial Controls */}
{partialControls.length > 0 && (
<div>
<h4 className="font-medium text-gray-900 mb-2">
Teilweise umgesetzt ({partialControls.length})
</h4>
<div className="space-y-2 max-h-48 overflow-y-auto">
{partialControls.map((pc) => {
const control = getControlById(pc.controlId)
return (
<div key={pc.controlId} className="p-2 bg-yellow-50 rounded-lg">
<div className="flex items-center gap-2">
<span className="font-mono text-sm text-gray-600">{pc.controlId}</span>
<span className="text-sm text-gray-900">{control?.name.de}</span>
</div>
<div className="text-xs text-yellow-700 mt-1">
Fehlend: {pc.missingAspects.join(', ')}
</div>
</div>
)
})}
</div>
</div>
)}
{/* Recommendations */}
{recommendations.length > 0 && (
<div>
<h4 className="font-medium text-gray-900 mb-2">Empfehlungen</h4>
<ul className="space-y-2">
{recommendations.map((rec, index) => (
<li key={index} className="flex items-start gap-2 text-sm text-gray-600">
<svg className="w-4 h-4 text-blue-500 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
</svg>
{rec}
</li>
))}
</ul>
</div>
)}
</div>
)
}
// =============================================================================
// EXPORT PANEL
// =============================================================================
export function ExportPanel() {
const { state, addExport } = useTOMGenerator()
const [isExporting, setIsExporting] = useState<string | null>(null)
const handleExport = async (format: 'docx' | 'pdf' | 'json' | 'zip') => {
setIsExporting(format)
try {
let blob: Blob
let filename: string
switch (format) {
case 'docx':
blob = await generateDOCXBlob(state, { language: 'de' })
filename = `TOM-Dokumentation-${new Date().toISOString().split('T')[0]}.docx`
break
case 'pdf':
blob = await generatePDFBlob(state, { language: 'de' })
filename = `TOM-Dokumentation-${new Date().toISOString().split('T')[0]}.pdf`
break
case 'json':
blob = new Blob([JSON.stringify(state, null, 2)], { type: 'application/json' })
filename = `TOM-Export-${new Date().toISOString().split('T')[0]}.json`
break
case 'zip':
blob = await generateZIPBlob(state, { language: 'de' })
filename = `TOM-Package-${new Date().toISOString().split('T')[0]}.zip`
break
default:
return
}
// Download
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = filename
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
// Record export
addExport({
id: `export-${Date.now()}`,
format: format.toUpperCase() as 'DOCX' | 'PDF' | 'JSON' | 'ZIP',
generatedAt: new Date(),
filename,
})
} catch (error) {
console.error('Export failed:', error)
} finally {
setIsExporting(null)
}
}
const exportFormats = [
{ id: 'docx', label: 'Word (.docx)', icon: '📄', description: 'Bearbeitbares Dokument' },
{ id: 'pdf', label: 'PDF', icon: '📕', description: 'Druckversion' },
{ id: 'json', label: 'JSON', icon: '💾', description: 'Maschinelles Format' },
{ id: 'zip', label: 'ZIP-Paket', icon: '📦', description: 'Vollständiges Paket' },
]
return (
<div className="space-y-4">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{exportFormats.map((format) => (
<button
key={format.id}
onClick={() => handleExport(format.id as 'docx' | 'pdf' | 'json' | 'zip')}
disabled={isExporting !== null}
className={`p-4 border rounded-lg text-center transition-all ${
isExporting === format.id
? 'bg-blue-50 border-blue-300'
: 'hover:bg-gray-50 hover:border-gray-300'
} disabled:opacity-50 disabled:cursor-not-allowed`}
>
<div className="text-3xl mb-2">{format.icon}</div>
<div className="font-medium text-gray-900">{format.label}</div>
<div className="text-xs text-gray-500">{format.description}</div>
{isExporting === format.id && (
<div className="mt-2">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600 mx-auto" />
</div>
)}
</button>
))}
</div>
{/* Export History */}
{state.exports.length > 0 && (
<div className="mt-6">
<h4 className="font-medium text-gray-900 mb-2">Letzte Exporte</h4>
<div className="space-y-2">
{state.exports.slice(-5).reverse().map((exp) => (
<div key={exp.id} className="flex items-center justify-between p-2 bg-gray-50 rounded-lg text-sm">
<span className="font-medium">{exp.filename}</span>
<span className="text-gray-500">
{new Date(exp.generatedAt).toLocaleString('de-DE')}
</span>
</div>
))}
</div>
</div>
)}
</div>
)
}

View File

@@ -5,430 +5,9 @@
// Summary, derived TOMs table, gap analysis, and export
// =============================================================================
import React, { useEffect, useState } from 'react'
import { useEffect, useState } from 'react'
import { useTOMGenerator } from '@/lib/sdk/tom-generator'
import { CONTROL_CATEGORIES } from '@/lib/sdk/tom-generator/types'
import { getControlById } from '@/lib/sdk/tom-generator/controls/loader'
import { generateDOCXBlob } from '@/lib/sdk/tom-generator/export/docx'
import { generatePDFBlob } from '@/lib/sdk/tom-generator/export/pdf'
import { generateZIPBlob } from '@/lib/sdk/tom-generator/export/zip'
// =============================================================================
// SUMMARY CARD COMPONENT
// =============================================================================
interface SummaryCardProps {
title: string
value: string | number
description?: string
variant?: 'default' | 'success' | 'warning' | 'danger'
}
function SummaryCard({ title, value, description, variant = 'default' }: SummaryCardProps) {
const colors = {
default: 'bg-gray-100 text-gray-800',
success: 'bg-green-100 text-green-800',
warning: 'bg-yellow-100 text-yellow-800',
danger: 'bg-red-100 text-red-800',
}
return (
<div className={`rounded-lg p-4 ${colors[variant]}`}>
<div className="text-2xl font-bold">{value}</div>
<div className="font-medium">{title}</div>
{description && <div className="text-sm opacity-75 mt-1">{description}</div>}
</div>
)
}
// =============================================================================
// TOMS TABLE COMPONENT
// =============================================================================
function TOMsTable() {
const { state } = useTOMGenerator()
const [selectedCategory, setSelectedCategory] = useState<string>('all')
const [selectedApplicability, setSelectedApplicability] = useState<string>('all')
const filteredTOMs = state.derivedTOMs.filter((tom) => {
const control = getControlById(tom.controlId)
const categoryMatch = selectedCategory === 'all' || control?.category === selectedCategory
const applicabilityMatch = selectedApplicability === 'all' || tom.applicability === selectedApplicability
return categoryMatch && applicabilityMatch
})
const getStatusBadge = (status: string) => {
const badges: Record<string, { bg: string; text: string }> = {
IMPLEMENTED: { bg: 'bg-green-100', text: 'text-green-800' },
PARTIAL: { bg: 'bg-yellow-100', text: 'text-yellow-800' },
NOT_IMPLEMENTED: { bg: 'bg-red-100', text: 'text-red-800' },
}
const labels: Record<string, string> = {
IMPLEMENTED: 'Umgesetzt',
PARTIAL: 'Teilweise',
NOT_IMPLEMENTED: 'Offen',
}
const config = badges[status] || badges.NOT_IMPLEMENTED
return (
<span className={`px-2 py-1 rounded-full text-xs font-medium ${config.bg} ${config.text}`}>
{labels[status] || status}
</span>
)
}
const getApplicabilityBadge = (applicability: string) => {
const badges: Record<string, { bg: string; text: string }> = {
REQUIRED: { bg: 'bg-red-100', text: 'text-red-800' },
RECOMMENDED: { bg: 'bg-blue-100', text: 'text-blue-800' },
OPTIONAL: { bg: 'bg-gray-100', text: 'text-gray-800' },
NOT_APPLICABLE: { bg: 'bg-gray-50', text: 'text-gray-500' },
}
const labels: Record<string, string> = {
REQUIRED: 'Erforderlich',
RECOMMENDED: 'Empfohlen',
OPTIONAL: 'Optional',
NOT_APPLICABLE: 'N/A',
}
const config = badges[applicability] || badges.OPTIONAL
return (
<span className={`px-2 py-1 rounded-full text-xs font-medium ${config.bg} ${config.text}`}>
{labels[applicability] || applicability}
</span>
)
}
return (
<div>
{/* Filters */}
<div className="flex flex-wrap gap-4 mb-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Kategorie</label>
<select
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
>
<option value="all">Alle Kategorien</option>
{CONTROL_CATEGORIES.map((cat) => (
<option key={cat.id} value={cat.id}>{cat.name.de}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Anwendbarkeit</label>
<select
value={selectedApplicability}
onChange={(e) => setSelectedApplicability(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
>
<option value="all">Alle</option>
<option value="REQUIRED">Erforderlich</option>
<option value="RECOMMENDED">Empfohlen</option>
<option value="OPTIONAL">Optional</option>
<option value="NOT_APPLICABLE">Nicht anwendbar</option>
</select>
</div>
</div>
{/* Table */}
<div className="overflow-x-auto border rounded-lg">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
ID
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Maßnahme
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Anwendbarkeit
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Nachweise
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{filteredTOMs.map((tom) => (
<tr key={tom.id} className="hover:bg-gray-50">
<td className="px-4 py-3 text-sm font-mono text-gray-900">
{tom.controlId}
</td>
<td className="px-4 py-3">
<div className="text-sm font-medium text-gray-900">{tom.name}</div>
<div className="text-xs text-gray-500 max-w-md truncate">{tom.applicabilityReason}</div>
</td>
<td className="px-4 py-3">
{getApplicabilityBadge(tom.applicability)}
</td>
<td className="px-4 py-3">
{getStatusBadge(tom.implementationStatus)}
</td>
<td className="px-4 py-3 text-sm text-gray-500">
{tom.linkedEvidence.length > 0 ? (
<span className="text-green-600">{tom.linkedEvidence.length} Dok.</span>
) : tom.evidenceGaps.length > 0 ? (
<span className="text-red-600">{tom.evidenceGaps.length} fehlen</span>
) : (
'-'
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="mt-2 text-sm text-gray-500">
{filteredTOMs.length} von {state.derivedTOMs.length} Maßnahmen angezeigt
</div>
</div>
)
}
// =============================================================================
// GAP ANALYSIS PANEL
// =============================================================================
function GapAnalysisPanel() {
const { state, runGapAnalysis } = useTOMGenerator()
useEffect(() => {
if (!state.gapAnalysis && state.derivedTOMs.length > 0) {
runGapAnalysis()
}
}, [state.derivedTOMs, state.gapAnalysis, runGapAnalysis])
if (!state.gapAnalysis) {
return (
<div className="text-center py-8 text-gray-500">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-2" />
Lückenanalyse wird durchgeführt...
</div>
)
}
const { overallScore, missingControls, partialControls, recommendations } = state.gapAnalysis
const getScoreColor = (score: number) => {
if (score >= 80) return 'text-green-600'
if (score >= 50) return 'text-yellow-600'
return 'text-red-600'
}
return (
<div className="space-y-6">
{/* Score */}
<div className="text-center">
<div className={`text-5xl font-bold ${getScoreColor(overallScore)}`}>
{overallScore}%
</div>
<div className="text-gray-600 mt-1">Compliance Score</div>
</div>
{/* Progress bar */}
<div className="h-4 bg-gray-200 rounded-full overflow-hidden">
<div
className={`h-full transition-all duration-500 ${
overallScore >= 80 ? 'bg-green-500' : overallScore >= 50 ? 'bg-yellow-500' : 'bg-red-500'
}`}
style={{ width: `${overallScore}%` }}
/>
</div>
{/* Missing Controls */}
{missingControls.length > 0 && (
<div>
<h4 className="font-medium text-gray-900 mb-2">
Fehlende Maßnahmen ({missingControls.length})
</h4>
<div className="space-y-2 max-h-48 overflow-y-auto">
{missingControls.map((mc) => {
const control = getControlById(mc.controlId)
return (
<div key={mc.controlId} className="flex items-center justify-between p-2 bg-red-50 rounded-lg">
<div>
<span className="font-mono text-sm text-gray-600">{mc.controlId}</span>
<span className="ml-2 text-sm text-gray-900">{control?.name.de}</span>
</div>
<span className={`px-2 py-0.5 text-xs rounded ${
mc.priority === 'CRITICAL' ? 'bg-red-200 text-red-800' :
mc.priority === 'HIGH' ? 'bg-orange-200 text-orange-800' :
'bg-gray-200 text-gray-800'
}`}>
{mc.priority}
</span>
</div>
)
})}
</div>
</div>
)}
{/* Partial Controls */}
{partialControls.length > 0 && (
<div>
<h4 className="font-medium text-gray-900 mb-2">
Teilweise umgesetzt ({partialControls.length})
</h4>
<div className="space-y-2 max-h-48 overflow-y-auto">
{partialControls.map((pc) => {
const control = getControlById(pc.controlId)
return (
<div key={pc.controlId} className="p-2 bg-yellow-50 rounded-lg">
<div className="flex items-center gap-2">
<span className="font-mono text-sm text-gray-600">{pc.controlId}</span>
<span className="text-sm text-gray-900">{control?.name.de}</span>
</div>
<div className="text-xs text-yellow-700 mt-1">
Fehlend: {pc.missingAspects.join(', ')}
</div>
</div>
)
})}
</div>
</div>
)}
{/* Recommendations */}
{recommendations.length > 0 && (
<div>
<h4 className="font-medium text-gray-900 mb-2">Empfehlungen</h4>
<ul className="space-y-2">
{recommendations.map((rec, index) => (
<li key={index} className="flex items-start gap-2 text-sm text-gray-600">
<svg className="w-4 h-4 text-blue-500 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
</svg>
{rec}
</li>
))}
</ul>
</div>
)}
</div>
)
}
// =============================================================================
// EXPORT PANEL
// =============================================================================
function ExportPanel() {
const { state, addExport } = useTOMGenerator()
const [isExporting, setIsExporting] = useState<string | null>(null)
const handleExport = async (format: 'docx' | 'pdf' | 'json' | 'zip') => {
setIsExporting(format)
try {
let blob: Blob
let filename: string
switch (format) {
case 'docx':
blob = await generateDOCXBlob(state, { language: 'de' })
filename = `TOM-Dokumentation-${new Date().toISOString().split('T')[0]}.docx`
break
case 'pdf':
blob = await generatePDFBlob(state, { language: 'de' })
filename = `TOM-Dokumentation-${new Date().toISOString().split('T')[0]}.pdf`
break
case 'json':
blob = new Blob([JSON.stringify(state, null, 2)], { type: 'application/json' })
filename = `TOM-Export-${new Date().toISOString().split('T')[0]}.json`
break
case 'zip':
blob = await generateZIPBlob(state, { language: 'de' })
filename = `TOM-Package-${new Date().toISOString().split('T')[0]}.zip`
break
default:
return
}
// Download
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = filename
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
// Record export
addExport({
id: `export-${Date.now()}`,
format: format.toUpperCase() as 'DOCX' | 'PDF' | 'JSON' | 'ZIP',
generatedAt: new Date(),
filename,
})
} catch (error) {
console.error('Export failed:', error)
} finally {
setIsExporting(null)
}
}
const exportFormats = [
{ id: 'docx', label: 'Word (.docx)', icon: '📄', description: 'Bearbeitbares Dokument' },
{ id: 'pdf', label: 'PDF', icon: '📕', description: 'Druckversion' },
{ id: 'json', label: 'JSON', icon: '💾', description: 'Maschinelles Format' },
{ id: 'zip', label: 'ZIP-Paket', icon: '📦', description: 'Vollständiges Paket' },
]
return (
<div className="space-y-4">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{exportFormats.map((format) => (
<button
key={format.id}
onClick={() => handleExport(format.id as 'docx' | 'pdf' | 'json' | 'zip')}
disabled={isExporting !== null}
className={`p-4 border rounded-lg text-center transition-all ${
isExporting === format.id
? 'bg-blue-50 border-blue-300'
: 'hover:bg-gray-50 hover:border-gray-300'
} disabled:opacity-50 disabled:cursor-not-allowed`}
>
<div className="text-3xl mb-2">{format.icon}</div>
<div className="font-medium text-gray-900">{format.label}</div>
<div className="text-xs text-gray-500">{format.description}</div>
{isExporting === format.id && (
<div className="mt-2">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600 mx-auto" />
</div>
)}
</button>
))}
</div>
{/* Export History */}
{state.exports.length > 0 && (
<div className="mt-6">
<h4 className="font-medium text-gray-900 mb-2">Letzte Exporte</h4>
<div className="space-y-2">
{state.exports.slice(-5).reverse().map((exp) => (
<div key={exp.id} className="flex items-center justify-between p-2 bg-gray-50 rounded-lg text-sm">
<span className="font-medium">{exp.filename}</span>
<span className="text-gray-500">
{new Date(exp.generatedAt).toLocaleString('de-DE')}
</span>
</div>
))}
</div>
</div>
)}
</div>
)
}
// =============================================================================
// MAIN COMPONENT
// =============================================================================
import { SummaryCard, TOMsTable, GapAnalysisPanel, ExportPanel } from './ReviewExportPanels'
export function ReviewExportStep() {
const { state, deriveTOMs, completeCurrentStep } = useTOMGenerator()
@@ -490,31 +69,11 @@ export function ReviewExportStep() {
<div className="space-y-6">
{/* Stats Grid */}
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
<SummaryCard
title="Gesamt TOMs"
value={stats.totalTOMs}
variant="default"
/>
<SummaryCard
title="Erforderlich"
value={stats.required}
variant="danger"
/>
<SummaryCard
title="Umgesetzt"
value={stats.implemented}
variant="success"
/>
<SummaryCard
title="Teilweise"
value={stats.partial}
variant="warning"
/>
<SummaryCard
title="Dokumente"
value={stats.documents}
variant="default"
/>
<SummaryCard title="Gesamt TOMs" value={stats.totalTOMs} variant="default" />
<SummaryCard title="Erforderlich" value={stats.required} variant="danger" />
<SummaryCard title="Umgesetzt" value={stats.implemented} variant="success" />
<SummaryCard title="Teilweise" value={stats.partial} variant="warning" />
<SummaryCard title="Dokumente" value={stats.documents} variant="default" />
<SummaryCard
title="Score"
value={`${stats.score}%`}