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:
@@ -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>
|
||||
)
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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('.')
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
40
admin-compliance/components/sdk/Sidebar/SidebarIcons.tsx
Normal file
40
admin-compliance/components/sdk/Sidebar/SidebarIcons.tsx
Normal 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>
|
||||
)
|
||||
108
admin-compliance/components/sdk/Sidebar/SidebarModuleList.tsx
Normal file
108
admin-compliance/components/sdk/Sidebar/SidebarModuleList.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
295
admin-compliance/components/sdk/Sidebar/SidebarSubComponents.tsx
Normal file
295
admin-compliance/components/sdk/Sidebar/SidebarSubComponents.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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.' },
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -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.' },
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
222
admin-compliance/components/sdk/source-policy/PIIRuleModals.tsx
Normal file
222
admin-compliance/components/sdk/source-policy/PIIRuleModals.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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">
|
||||
×
|
||||
</button>
|
||||
<button onClick={() => setError(null)} className="text-red-500 hover:text-red-700">×</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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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}%`}
|
||||
|
||||
Reference in New Issue
Block a user