refactor(admin): split SDKSidebar and ScopeWizardTab components
Some checks failed
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 49s
CI/CD / test-python-backend-compliance (push) Successful in 46s
CI/CD / test-python-document-crawler (push) Successful in 30s
CI/CD / test-python-dsms-gateway (push) Successful in 31s
CI/CD / validate-canonical-controls (push) Successful in 23s
CI/CD / Deploy (push) Failing after 7s
Some checks failed
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 49s
CI/CD / test-python-backend-compliance (push) Successful in 46s
CI/CD / test-python-document-crawler (push) Successful in 30s
CI/CD / test-python-dsms-gateway (push) Successful in 31s
CI/CD / validate-canonical-controls (push) Successful in 23s
CI/CD / Deploy (push) Failing after 7s
- SDKSidebar (918→236 LOC): extracted icons to SidebarIcons, sub-components (ProgressBar, PackageIndicator, StepItem, CorpusStalenessInfo, AdditionalModuleItem) to SidebarSubComponents, and the full module nav list to SidebarModuleNav - ScopeWizardTab (794→339 LOC): extracted DatenkategorienBlock9 and its dept mapping constants to DatenkategorienBlock, and question rendering (all switch-case types + help text) to ScopeQuestionRenderer - All files now under 500 LOC hard cap; zero behavior changes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,346 +10,21 @@ import {
|
|||||||
getStepsForPackage,
|
getStepsForPackage,
|
||||||
type SDKPackageId,
|
type SDKPackageId,
|
||||||
type SDKStep,
|
type SDKStep,
|
||||||
type RAGCorpusStatus,
|
|
||||||
} from '@/lib/sdk'
|
} from '@/lib/sdk'
|
||||||
|
import { CollapseIcon } from './SidebarIcons'
|
||||||
/**
|
import {
|
||||||
* Append ?project= to a URL if a projectId is set
|
ProgressBar,
|
||||||
*/
|
PackageIndicator,
|
||||||
function withProject(url: string, projectId?: string): string {
|
StepItem,
|
||||||
if (!projectId) return url
|
CorpusStalenessInfo,
|
||||||
const separator = url.includes('?') ? '&' : '?'
|
} from './SidebarSubComponents'
|
||||||
return `${url}${separator}project=${projectId}`
|
import { SidebarModuleNav } from './SidebarModuleNav'
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// 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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// MAIN SIDEBAR
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
interface SDKSidebarProps {
|
interface SDKSidebarProps {
|
||||||
collapsed?: boolean
|
collapsed?: boolean
|
||||||
onCollapsedChange?: (collapsed: boolean) => void
|
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) {
|
export function SDKSidebar({ collapsed = false, onCollapsedChange }: SDKSidebarProps) {
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
const { state, packageCompletion, completionPercentage, getCheckpointStatus, projectId } = useSDK()
|
const { state, packageCompletion, completionPercentage, getCheckpointStatus, projectId } = useSDK()
|
||||||
@@ -404,11 +79,8 @@ export function SDKSidebar({ collapsed = false, onCollapsedChange }: SDKSidebarP
|
|||||||
if (state.preferences?.allowParallelWork) return false
|
if (state.preferences?.allowParallelWork) return false
|
||||||
const pkg = SDK_PACKAGES.find(p => p.id === packageId)
|
const pkg = SDK_PACKAGES.find(p => p.id === packageId)
|
||||||
if (!pkg || pkg.order === 1) return false
|
if (!pkg || pkg.order === 1) return false
|
||||||
|
|
||||||
// Check if previous package is complete
|
|
||||||
const prevPkg = SDK_PACKAGES.find(p => p.order === pkg.order - 1)
|
const prevPkg = SDK_PACKAGES.find(p => p.order === pkg.order - 1)
|
||||||
if (!prevPkg) return false
|
if (!prevPkg) return false
|
||||||
|
|
||||||
return packageCompletion[prevPkg.id] < 100
|
return packageCompletion[prevPkg.id] < 100
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -428,7 +100,6 @@ export function SDKSidebar({ collapsed = false, onCollapsedChange }: SDKSidebarP
|
|||||||
return steps.some(s => s.url === pathname)
|
return steps.some(s => s.url === pathname)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter steps based on visibleWhen conditions
|
|
||||||
const getVisibleStepsForPackage = (packageId: SDKPackageId): SDKStep[] => {
|
const getVisibleStepsForPackage = (packageId: SDKPackageId): SDKStep[] => {
|
||||||
const steps = getStepsForPackage(packageId)
|
const steps = getStepsForPackage(packageId)
|
||||||
return steps.filter(step => {
|
return steps.filter(step => {
|
||||||
@@ -524,368 +195,16 @@ export function SDKSidebar({ collapsed = false, onCollapsedChange }: SDKSidebarP
|
|||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{/* Maschinenrecht / CE */}
|
<SidebarModuleNav
|
||||||
<div className="border-t border-gray-100 py-2">
|
pathname={pathname}
|
||||||
{!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}
|
collapsed={collapsed}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
|
pendingCRCount={pendingCRCount}
|
||||||
/>
|
/>
|
||||||
</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>
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className={`${collapsed ? 'p-2' : 'p-4'} border-t border-gray-200 bg-gray-50`}>
|
<div className={`${collapsed ? 'p-2' : 'p-4'} border-t border-gray-200 bg-gray-50`}>
|
||||||
{/* Collapse Toggle */}
|
|
||||||
<button
|
<button
|
||||||
onClick={() => onCollapsedChange?.(!collapsed)}
|
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'}`}
|
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,7 +214,6 @@ export function SDKSidebar({ collapsed = false, onCollapsedChange }: SDKSidebarP
|
|||||||
{!collapsed && <span>Einklappen</span>}
|
{!collapsed && <span>Einklappen</span>}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Export Button */}
|
|
||||||
{!collapsed && (
|
{!collapsed && (
|
||||||
<button
|
<button
|
||||||
onClick={() => {}}
|
onClick={() => {}}
|
||||||
|
|||||||
36
admin-compliance/components/sdk/Sidebar/SidebarIcons.tsx
Normal file
36
admin-compliance/components/sdk/Sidebar/SidebarIcons.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
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>
|
||||||
|
)
|
||||||
368
admin-compliance/components/sdk/Sidebar/SidebarModuleNav.tsx
Normal file
368
admin-compliance/components/sdk/Sidebar/SidebarModuleNav.tsx
Normal file
@@ -0,0 +1,368 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { AdditionalModuleItem } from './SidebarSubComponents'
|
||||||
|
|
||||||
|
function withProject(url: string, projectId?: string): string {
|
||||||
|
if (!projectId) return url
|
||||||
|
const separator = url.includes('?') ? '&' : '?'
|
||||||
|
return `${url}${separator}project=${projectId}`
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SidebarModuleNavProps {
|
||||||
|
pathname: string | null
|
||||||
|
collapsed: boolean
|
||||||
|
projectId?: string
|
||||||
|
pendingCRCount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SidebarModuleNav({ pathname, collapsed, projectId, pendingCRCount }: SidebarModuleNavProps) {
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
<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>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
282
admin-compliance/components/sdk/Sidebar/SidebarSubComponents.tsx
Normal file
282
admin-compliance/components/sdk/Sidebar/SidebarSubComponents.tsx
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import type { SDKStep, RAGCorpusStatus, SDKPackageId } from '@/lib/sdk'
|
||||||
|
import { CheckIcon, LockIcon, WarningIcon, ChevronDownIcon } from './SidebarIcons'
|
||||||
|
|
||||||
|
function withProject(url: string, projectId?: string): string {
|
||||||
|
if (!projectId) return url
|
||||||
|
const separator = url.includes('?') ? '&' : '?'
|
||||||
|
return `${url}${separator}project=${projectId}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// PROGRESS BAR
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// PACKAGE INDICATOR
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// STEP ITEM
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// ADDITIONAL MODULE ITEM
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
interface AdditionalModuleItemProps {
|
||||||
|
href: string
|
||||||
|
icon: React.ReactNode
|
||||||
|
label: string
|
||||||
|
isActive: boolean | undefined
|
||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// CORPUS STALENESS INFO
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
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,225 @@
|
|||||||
|
'use client'
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import type { ScopeProfilingAnswer } from '@/lib/sdk/compliance-scope-types'
|
||||||
|
import { DEPARTMENT_DATA_CATEGORIES } from '@/lib/sdk/vvt-profiling'
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// CONSTANTS
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/** 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',
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// DATENKATEGORIEN BLOCK 9
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
'use client'
|
||||||
|
import React from 'react'
|
||||||
|
import type { ScopeProfilingAnswer, ScopeProfilingQuestion } from '@/lib/sdk/compliance-scope-types'
|
||||||
|
import { getAnswerValue } from '@/lib/sdk/compliance-scope-profiling'
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// HELP TEXT
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
interface HelpTextProps {
|
||||||
|
question: ScopeProfilingQuestion
|
||||||
|
expandedHelp: Set<string>
|
||||||
|
onToggleHelp: (questionId: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function QuestionHelpText({ question, expandedHelp, onToggleHelp }: HelpTextProps) {
|
||||||
|
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(); onToggleHelp(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>
|
||||||
|
<span>{question.helpText}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// QUESTION RENDERER
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
interface ScopeQuestionRendererProps {
|
||||||
|
question: ScopeProfilingQuestion
|
||||||
|
answers: ScopeProfilingAnswer[]
|
||||||
|
prefilledIds: Set<string>
|
||||||
|
expandedHelp: Set<string>
|
||||||
|
onAnswerChange: (questionId: string, value: string | string[] | boolean | number) => void
|
||||||
|
onToggleHelp: (questionId: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ScopeQuestionRenderer({
|
||||||
|
question,
|
||||||
|
answers,
|
||||||
|
prefilledIds,
|
||||||
|
expandedHelp,
|
||||||
|
onAnswerChange,
|
||||||
|
onToggleHelp,
|
||||||
|
}: ScopeQuestionRendererProps) {
|
||||||
|
const currentValue = getAnswerValue(answers, question.id)
|
||||||
|
const isPrefilled = prefilledIds.has(question.id)
|
||||||
|
|
||||||
|
const labelRow = (
|
||||||
|
<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>}
|
||||||
|
{isPrefilled && (
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
|
<QuestionHelpText question={question} expandedHelp={expandedHelp} onToggleHelp={onToggleHelp} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
switch (question.type) {
|
||||||
|
case 'boolean':
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-start justify-between">{labelRow}</div>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
{([true, false] as const).map(val => (
|
||||||
|
<button
|
||||||
|
key={String(val)}
|
||||||
|
type="button"
|
||||||
|
onClick={() => onAnswerChange(question.id, val)}
|
||||||
|
className={`flex-1 py-2 px-4 rounded-lg border-2 transition-all ${
|
||||||
|
currentValue === val
|
||||||
|
? 'border-purple-500 bg-purple-50 text-purple-700 font-medium'
|
||||||
|
: 'border-gray-300 bg-white text-gray-700 hover:border-gray-400'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{val ? 'Ja' : 'Nein'}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
case 'single':
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{labelRow}
|
||||||
|
<div className="space-y-2">
|
||||||
|
{question.options?.map((option) => (
|
||||||
|
<button
|
||||||
|
key={option.value}
|
||||||
|
type="button"
|
||||||
|
onClick={() => onAnswerChange(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': {
|
||||||
|
const selectedValues = Array.isArray(currentValue) ? currentValue as string[] : []
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{labelRow}
|
||||||
|
<div className="space-y-2">
|
||||||
|
{question.options?.map((option) => {
|
||||||
|
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)
|
||||||
|
onAnswerChange(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">
|
||||||
|
{labelRow}
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={currentValue != null ? String(currentValue) : ''}
|
||||||
|
onChange={(e) => onAnswerChange(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">
|
||||||
|
{labelRow}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={currentValue != null ? String(currentValue) : ''}
|
||||||
|
onChange={(e) => onAnswerChange(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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
'use client'
|
'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 type { ScopeProfilingAnswer } from '@/lib/sdk/compliance-scope-types'
|
||||||
import { SCOPE_QUESTION_BLOCKS, getBlockProgress, getTotalProgress, getAnswerValue, prefillFromCompanyProfile, getProfileInfoForBlock, getAutoFilledScoringAnswers, getUnansweredRequiredQuestions } from '@/lib/sdk/compliance-scope-profiling'
|
import { SCOPE_QUESTION_BLOCKS, getBlockProgress, getTotalProgress, 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 type { ScopeQuestionBlockId } from '@/lib/sdk/compliance-scope-types'
|
||||||
import { useSDK } from '@/lib/sdk'
|
import { useSDK } from '@/lib/sdk'
|
||||||
|
import { DatenkategorienBlock9 } from './DatenkategorienBlock'
|
||||||
|
import { ScopeQuestionRenderer } from './ScopeQuestionRenderer'
|
||||||
|
|
||||||
interface ScopeWizardTabProps {
|
interface ScopeWizardTabProps {
|
||||||
answers: ScopeProfilingAnswer[]
|
answers: ScopeProfilingAnswer[]
|
||||||
@@ -28,18 +29,15 @@ export function ScopeWizardTab({
|
|||||||
const currentBlock = SCOPE_QUESTION_BLOCKS[currentBlockIndex]
|
const currentBlock = SCOPE_QUESTION_BLOCKS[currentBlockIndex]
|
||||||
const totalProgress = getTotalProgress(answers)
|
const totalProgress = getTotalProgress(answers)
|
||||||
|
|
||||||
// Load companyProfile from SDK context
|
|
||||||
const { state: sdkState } = useSDK()
|
const { state: sdkState } = useSDK()
|
||||||
const companyProfile = sdkState.companyProfile
|
const companyProfile = sdkState.companyProfile
|
||||||
|
|
||||||
// Track which question IDs were prefilled from profile
|
|
||||||
const [prefilledIds, setPrefilledIds] = useState<Set<string>>(new Set())
|
const [prefilledIds, setPrefilledIds] = useState<Set<string>>(new Set())
|
||||||
|
|
||||||
// Auto-prefill from company profile on mount if answers are empty
|
// Auto-prefill from company profile on mount if answers are empty
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (companyProfile && answers.length === 0) {
|
if (companyProfile && answers.length === 0) {
|
||||||
const prefilled = prefillFromCompanyProfile(companyProfile)
|
const prefilled = prefillFromCompanyProfile(companyProfile)
|
||||||
// Also inject auto-filled scoring answers for questions removed from UI
|
|
||||||
const autoFilled = getAutoFilledScoringAnswers(companyProfile)
|
const autoFilled = getAutoFilledScoringAnswers(companyProfile)
|
||||||
const allPrefilled = [...prefilled, ...autoFilled]
|
const allPrefilled = [...prefilled, ...autoFilled]
|
||||||
if (allPrefilled.length > 0) {
|
if (allPrefilled.length > 0) {
|
||||||
@@ -47,7 +45,6 @@ export function ScopeWizardTab({
|
|||||||
setPrefilledIds(new Set(allPrefilled.map(a => a.questionId)))
|
setPrefilledIds(new Set(allPrefilled.map(a => a.questionId)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Only run on mount
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@@ -61,7 +58,6 @@ export function ScopeWizardTab({
|
|||||||
} else {
|
} else {
|
||||||
onAnswersChange([...answers, { questionId, value }])
|
onAnswersChange([...answers, { questionId, value }])
|
||||||
}
|
}
|
||||||
// Remove from prefilled set when user manually changes
|
|
||||||
if (prefilledIds.has(questionId)) {
|
if (prefilledIds.has(questionId)) {
|
||||||
setPrefilledIds(prev => {
|
setPrefilledIds(prev => {
|
||||||
const next = new Set(prev)
|
const next = new Set(prev)
|
||||||
@@ -78,7 +74,6 @@ export function ScopeWizardTab({
|
|||||||
const prefilled = prefillFromCompanyProfile(companyProfile)
|
const prefilled = prefillFromCompanyProfile(companyProfile)
|
||||||
const autoFilled = getAutoFilledScoringAnswers(companyProfile)
|
const autoFilled = getAutoFilledScoringAnswers(companyProfile)
|
||||||
const allPrefilled = [...prefilled, ...autoFilled]
|
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 existingIds = new Set(answers.map(a => a.questionId))
|
||||||
const newAnswers = [...answers]
|
const newAnswers = [...answers]
|
||||||
const newPrefilledIds = new Set(prefilledIds)
|
const newPrefilledIds = new Set(prefilledIds)
|
||||||
@@ -101,242 +96,18 @@ export function ScopeWizardTab({
|
|||||||
}, [currentBlockIndex, canEvaluate, onEvaluate])
|
}, [currentBlockIndex, canEvaluate, onEvaluate])
|
||||||
|
|
||||||
const handleBack = useCallback(() => {
|
const handleBack = useCallback(() => {
|
||||||
if (currentBlockIndex > 0) {
|
if (currentBlockIndex > 0) setCurrentBlockIndex(currentBlockIndex - 1)
|
||||||
setCurrentBlockIndex(currentBlockIndex - 1)
|
|
||||||
}
|
|
||||||
}, [currentBlockIndex])
|
}, [currentBlockIndex])
|
||||||
|
|
||||||
const toggleHelp = useCallback((questionId: string) => {
|
const toggleHelp = useCallback((questionId: string) => {
|
||||||
setExpandedHelp(prev => {
|
setExpandedHelp(prev => {
|
||||||
const next = new Set(prev)
|
const next = new Set(prev)
|
||||||
if (next.has(questionId)) {
|
if (next.has(questionId)) next.delete(questionId)
|
||||||
next.delete(questionId)
|
else next.add(questionId)
|
||||||
} else {
|
|
||||||
next.add(questionId)
|
|
||||||
}
|
|
||||||
return next
|
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>
|
|
||||||
{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>
|
|
||||||
<span>{question.helpText}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderQuestion = (question: ScopeProfilingQuestion) => {
|
|
||||||
const currentValue = getAnswerValue(answers, question.id)
|
|
||||||
|
|
||||||
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 gap-3">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => handleAnswerChange(question.id, true)}
|
|
||||||
className={`flex-1 py-2 px-4 rounded-lg border-2 transition-all ${
|
|
||||||
currentValue === true
|
|
||||||
? 'border-purple-500 bg-purple-50 text-purple-700 font-medium'
|
|
||||||
: 'border-gray-300 bg-white text-gray-700 hover:border-gray-400'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Ja
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => handleAnswerChange(question.id, false)}
|
|
||||||
className={`flex-1 py-2 px-4 rounded-lg border-2 transition-all ${
|
|
||||||
currentValue === false
|
|
||||||
? 'border-purple-500 bg-purple-50 text-purple-700 font-medium'
|
|
||||||
: 'border-gray-300 bg-white text-gray-700 hover:border-gray-400'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Nein
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
case 'single':
|
|
||||||
return (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<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 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>
|
|
||||||
))}
|
|
||||||
</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>
|
|
||||||
<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>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</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"
|
|
||||||
/>
|
|
||||||
</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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
default:
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-6 h-full">
|
<div className="flex gap-6 h-full">
|
||||||
{/* Left Sidebar - Block Navigation */}
|
{/* Left Sidebar - Block Navigation */}
|
||||||
@@ -350,7 +121,6 @@ export function ScopeWizardTab({
|
|||||||
const unanswered = getUnansweredRequiredQuestions(answers, block.id)
|
const unanswered = getUnansweredRequiredQuestions(answers, block.id)
|
||||||
const hasRequired = block.questions.some(q => q.required)
|
const hasRequired = block.questions.some(q => q.required)
|
||||||
const allRequiredDone = hasRequired && unanswered.length === 0
|
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 answeredIds = new Set(answers.map(a => a.questionId))
|
||||||
const hasAnyAnswer = block.questions.some(q => answeredIds.has(q.id))
|
const hasAnyAnswer = block.questions.some(q => answeredIds.has(q.id))
|
||||||
const optionalDone = !hasRequired && hasAnyAnswer
|
const optionalDone = !hasRequired && hasAnyAnswer
|
||||||
@@ -380,19 +150,13 @@ export function ScopeWizardTab({
|
|||||||
) : !hasRequired ? (
|
) : !hasRequired ? (
|
||||||
<span className="text-xs text-gray-400">(nur optional)</span>
|
<span className="text-xs text-gray-400">(nur optional)</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-xs font-semibold text-orange-600">
|
<span className="text-xs font-semibold text-orange-600">{unanswered.length} offen</span>
|
||||||
{unanswered.length} offen
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full bg-gray-200 rounded-full h-1.5 overflow-hidden">
|
<div className="w-full bg-gray-200 rounded-full h-1.5 overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className={`h-full transition-all ${
|
className={`h-full transition-all ${
|
||||||
allRequiredDone || optionalDone
|
allRequiredDone || optionalDone ? 'bg-green-500' : !hasRequired ? 'bg-gray-300' : 'bg-orange-400'
|
||||||
? 'bg-green-500'
|
|
||||||
: !hasRequired
|
|
||||||
? 'bg-gray-300'
|
|
||||||
: 'bg-orange-400'
|
|
||||||
}`}
|
}`}
|
||||||
style={{ width: `${progress}%` }}
|
style={{ width: `${progress}%` }}
|
||||||
/>
|
/>
|
||||||
@@ -428,8 +192,6 @@ export function ScopeWizardTab({
|
|||||||
{(() => {
|
{(() => {
|
||||||
const allUnanswered = getUnansweredRequiredQuestions(answers)
|
const allUnanswered = getUnansweredRequiredQuestions(answers)
|
||||||
if (allUnanswered.length === 0) return null
|
if (allUnanswered.length === 0) return null
|
||||||
|
|
||||||
// Group by block
|
|
||||||
const byBlock = new Map<string, { blockTitle: string; blockIndex: number; count: number }>()
|
const byBlock = new Map<string, { blockTitle: string; blockIndex: number; count: number }>()
|
||||||
for (const item of allUnanswered) {
|
for (const item of allUnanswered) {
|
||||||
if (!byBlock.has(item.blockId)) {
|
if (!byBlock.has(item.blockId)) {
|
||||||
@@ -438,7 +200,6 @@ export function ScopeWizardTab({
|
|||||||
}
|
}
|
||||||
byBlock.get(item.blockId)!.count++
|
byBlock.get(item.blockId)!.count++
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-3 flex flex-wrap items-center gap-1.5 text-xs">
|
<div className="mt-3 flex flex-wrap items-center gap-1.5 text-xs">
|
||||||
<span className="text-orange-600 font-medium">⚠ Offene Pflichtfragen:</span>
|
<span className="text-orange-600 font-medium">⚠ Offene Pflichtfragen:</span>
|
||||||
@@ -477,7 +238,7 @@ export function ScopeWizardTab({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* "Aus Profil" Info Box — shown for blocks that have auto-filled data */}
|
{/* "Aus Profil" Info Box */}
|
||||||
{companyProfile && (() => {
|
{companyProfile && (() => {
|
||||||
const profileItems = getProfileInfoForBlock(companyProfile, currentBlock.id as ScopeQuestionBlockId)
|
const profileItems = getProfileInfoForBlock(companyProfile, currentBlock.id as ScopeQuestionBlockId)
|
||||||
if (profileItems.length === 0) return null
|
if (profileItems.length === 0) return null
|
||||||
@@ -516,21 +277,23 @@ export function ScopeWizardTab({
|
|||||||
{/* Questions */}
|
{/* Questions */}
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{currentBlock.id === 'datenkategorien_detail' ? (
|
{currentBlock.id === 'datenkategorien_detail' ? (
|
||||||
<DatenkategorienBlock9
|
<DatenkategorienBlock9 answers={answers} onAnswerChange={handleAnswerChange} />
|
||||||
answers={answers}
|
|
||||||
onAnswerChange={handleAnswerChange}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
currentBlock.questions.map((question) => {
|
currentBlock.questions.map((question) => {
|
||||||
const isAnswered = answers.some(a => a.questionId === question.id)
|
const isAnswered = answers.some(a => a.questionId === question.id)
|
||||||
const borderClass = question.required
|
const borderClass = question.required
|
||||||
? isAnswered
|
? isAnswered ? 'border-l-4 border-l-green-400 pl-4' : 'border-l-4 border-l-orange-400 pl-4'
|
||||||
? 'border-l-4 border-l-green-400 pl-4'
|
|
||||||
: 'border-l-4 border-l-orange-400 pl-4'
|
|
||||||
: ''
|
: ''
|
||||||
return (
|
return (
|
||||||
<div key={question.id} className={`border-b border-gray-100 pb-6 last:border-b-0 last:pb-0 ${borderClass}`}>
|
<div key={question.id} className={`border-b border-gray-100 pb-6 last:border-b-0 last:pb-0 ${borderClass}`}>
|
||||||
{renderQuestion(question)}
|
<ScopeQuestionRenderer
|
||||||
|
question={question}
|
||||||
|
answers={answers}
|
||||||
|
prefilledIds={prefilledIds}
|
||||||
|
expandedHelp={expandedHelp}
|
||||||
|
onAnswerChange={handleAnswerChange}
|
||||||
|
onToggleHelp={toggleHelp}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -574,221 +337,3 @@ export function ScopeWizardTab({
|
|||||||
</div>
|
</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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user