From 90d14eb54667aae84fd3195a8876442f35ae8e12 Mon Sep 17 00:00:00 2001 From: Sharang Parnerkar <30073382+mighty840@users.noreply.github.com> Date: Thu, 16 Apr 2026 23:03:46 +0200 Subject: [PATCH] refactor(admin): split SDKSidebar and ScopeWizardTab components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../components/sdk/Sidebar/SDKSidebar.tsx | 710 +----------------- .../components/sdk/Sidebar/SidebarIcons.tsx | 36 + .../sdk/Sidebar/SidebarModuleNav.tsx | 368 +++++++++ .../sdk/Sidebar/SidebarSubComponents.tsx | 282 +++++++ .../compliance-scope/DatenkategorienBlock.tsx | 225 ++++++ .../ScopeQuestionRenderer.tsx | 195 +++++ .../sdk/compliance-scope/ScopeWizardTab.tsx | 497 +----------- 7 files changed, 1141 insertions(+), 1172 deletions(-) create mode 100644 admin-compliance/components/sdk/Sidebar/SidebarIcons.tsx create mode 100644 admin-compliance/components/sdk/Sidebar/SidebarModuleNav.tsx create mode 100644 admin-compliance/components/sdk/Sidebar/SidebarSubComponents.tsx create mode 100644 admin-compliance/components/sdk/compliance-scope/DatenkategorienBlock.tsx create mode 100644 admin-compliance/components/sdk/compliance-scope/ScopeQuestionRenderer.tsx diff --git a/admin-compliance/components/sdk/Sidebar/SDKSidebar.tsx b/admin-compliance/components/sdk/Sidebar/SDKSidebar.tsx index 5080a76..980abac 100644 --- a/admin-compliance/components/sdk/Sidebar/SDKSidebar.tsx +++ b/admin-compliance/components/sdk/Sidebar/SDKSidebar.tsx @@ -10,346 +10,21 @@ import { getStepsForPackage, type SDKPackageId, type SDKStep, - type RAGCorpusStatus, } from '@/lib/sdk' - -/** - * Append ?project= to a URL if a projectId is set - */ -function withProject(url: string, projectId?: string): string { - if (!projectId) return url - const separator = url.includes('?') ? '&' : '?' - return `${url}${separator}project=${projectId}` -} - -// ============================================================================= -// ICONS -// ============================================================================= - -const CheckIcon = () => ( - - - -) - -const LockIcon = () => ( - - - -) - -const WarningIcon = () => ( - - - -) - -const ChevronDownIcon = ({ className = '' }: { className?: string }) => ( - - - -) - -const CollapseIcon = ({ collapsed }: { collapsed: boolean }) => ( - - - -) - -// ============================================================================= -// PROGRESS BAR -// ============================================================================= - -interface ProgressBarProps { - value: number - className?: string -} - -function ProgressBar({ value, className = '' }: ProgressBarProps) { - return ( -
-
-
- ) -} - -// ============================================================================= -// 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 ( - - ) - } - - return ( - - ) -} - -// ============================================================================= -// 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 = ( -
- {/* Step indicator */} -
- {isCompleted ? ( -
- -
- ) : isLocked ? ( -
- -
- ) : isActive ? ( -
-
-
- ) : ( -
- )} -
- - {/* Step name - hidden when collapsed */} - {!collapsed && {step.nameShort}} - - {/* Checkpoint status - hidden when collapsed */} - {!collapsed && checkpointStatus && checkpointStatus !== 'pending' && ( -
- {checkpointStatus === 'passed' ? ( -
- -
- ) : checkpointStatus === 'failed' ? ( -
- ! -
- ) : ( -
- -
- )} -
- )} -
- ) - - if (isLocked) { - return content - } - - return ( - - {content} - - ) -} - -// ============================================================================= -// 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 ( - - {icon} - {!collapsed && ( - - {label} - - - - - )} - - ) - } - - return ( - - {icon} - {!collapsed && {label}} - - ) -} - -// ============================================================================= -// MAIN SIDEBAR -// ============================================================================= +import { CollapseIcon } from './SidebarIcons' +import { + ProgressBar, + PackageIndicator, + StepItem, + CorpusStalenessInfo, +} from './SidebarSubComponents' +import { SidebarModuleNav } from './SidebarModuleNav' interface SDKSidebarProps { collapsed?: boolean 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 ( -
-
-
30 ? 'bg-amber-400' : 'bg-green-400'}`} /> - - RAG Corpus: {totalChunks} Chunks - -
- {daysSinceUpdate > 30 && ( -
- Corpus {daysSinceUpdate}d alt — Re-Evaluation empfohlen -
- )} -
- ) -} - export function SDKSidebar({ collapsed = false, onCollapsedChange }: SDKSidebarProps) { const pathname = usePathname() const { state, packageCompletion, completionPercentage, getCheckpointStatus, projectId } = useSDK() @@ -404,11 +79,8 @@ export function SDKSidebar({ collapsed = false, onCollapsedChange }: SDKSidebarP if (state.preferences?.allowParallelWork) return false const pkg = SDK_PACKAGES.find(p => p.id === packageId) if (!pkg || pkg.order === 1) return false - - // Check if previous package is complete const prevPkg = SDK_PACKAGES.find(p => p.order === pkg.order - 1) if (!prevPkg) return false - return packageCompletion[prevPkg.id] < 100 } @@ -428,7 +100,6 @@ export function SDKSidebar({ collapsed = false, onCollapsedChange }: SDKSidebarP return steps.some(s => s.url === pathname) } - // Filter steps based on visibleWhen conditions const getVisibleStepsForPackage = (packageId: SDKPackageId): SDKStep[] => { const steps = getStepsForPackage(packageId) return steps.filter(step => { @@ -524,368 +195,16 @@ export function SDKSidebar({ collapsed = false, onCollapsedChange }: SDKSidebarP ) })} - {/* Maschinenrecht / CE */} -
- {!collapsed && ( -
- Maschinenrecht / CE -
- )} - - - - } - label="CE-Compliance (IACE)" - isActive={pathname?.startsWith('/sdk/iace') ?? false} - collapsed={collapsed} - projectId={projectId} - /> -
- - {/* Additional Modules */} -
- {!collapsed && ( -
- Zusatzmodule -
- )} - - - - } - label="Schulung (Admin)" - isActive={pathname === '/sdk/training'} - collapsed={collapsed} - projectId={projectId} - /> - - - - } - label="Schulung (Learner)" - isActive={pathname === '/sdk/training/learner'} - collapsed={collapsed} - projectId={projectId} - /> - - - - } - label="Legal RAG" - isActive={pathname === '/sdk/rag'} - collapsed={collapsed} - projectId={projectId} - /> - - - - } - label="AI Quality" - isActive={pathname === '/sdk/quality'} - collapsed={collapsed} - projectId={projectId} - /> - - - - } - label="Security Backlog" - isActive={pathname === '/sdk/security-backlog'} - collapsed={collapsed} - projectId={projectId} - /> - - - - } - label="Compliance Hub" - isActive={pathname === '/sdk/compliance-hub'} - collapsed={collapsed} - projectId={projectId} - /> - - - - } - label="Assertions" - isActive={pathname === '/sdk/assertions'} - collapsed={collapsed} - projectId={projectId} - /> - - - - } - label="DSMS" - isActive={pathname === '/sdk/dsms'} - collapsed={collapsed} - projectId={projectId} - /> - - - - } - label="SDK Flow" - isActive={pathname === '/sdk/sdk-flow'} - collapsed={collapsed} - projectId={projectId} - /> - - - - } - label="Architektur" - isActive={pathname === '/sdk/architecture'} - collapsed={collapsed} - projectId={projectId} - /> - - - - } - label="Agenten" - isActive={pathname?.startsWith('/sdk/agents') ?? false} - collapsed={collapsed} - projectId={projectId} - /> - - - - } - label="Workshop" - isActive={pathname === '/sdk/workshop'} - collapsed={collapsed} - projectId={projectId} - /> - - - - } - label="Portfolio" - isActive={pathname === '/sdk/portfolio'} - collapsed={collapsed} - projectId={projectId} - /> - - - - } - label="Roadmap" - isActive={pathname === '/sdk/roadmap'} - collapsed={collapsed} - projectId={projectId} - /> - - - - } - label="ISMS (ISO 27001)" - isActive={pathname === '/sdk/isms'} - collapsed={collapsed} - projectId={projectId} - /> - - - - } - label="LLM Audit" - isActive={pathname === '/sdk/audit-llm'} - collapsed={collapsed} - projectId={projectId} - /> - - - - } - label="RBAC Admin" - isActive={pathname === '/sdk/rbac'} - collapsed={collapsed} - projectId={projectId} - /> - - - - } - label="Kataloge" - isActive={pathname === '/sdk/catalog-manager'} - collapsed={collapsed} - projectId={projectId} - /> - - - - } - label="Compliance Wiki" - isActive={pathname?.startsWith('/sdk/wiki')} - collapsed={collapsed} - projectId={projectId} - /> - - - - } - label="API-Referenz" - isActive={pathname === '/sdk/api-docs'} - collapsed={collapsed} - projectId={projectId} - /> - - - - - {!collapsed && ( - - Änderungsanfragen - {pendingCRCount > 0 && ( - - {pendingCRCount} - - )} - - )} - {collapsed && pendingCRCount > 0 && ( - - )} - - - - - } - label="Developer Portal" - isActive={false} - collapsed={collapsed} - projectId={projectId} - /> - - - - } - label="SDK Dokumentation" - isActive={false} - collapsed={collapsed} - projectId={projectId} - /> -
+ {/* Footer */}
- {/* Collapse Toggle */} - {/* Export Button */} {!collapsed && ( + ) + } + + return ( + + ) +} + +// ============================================================================= +// 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 = ( +
+
+ {isCompleted ? ( +
+ +
+ ) : isLocked ? ( +
+ +
+ ) : isActive ? ( +
+
+
+ ) : ( +
+ )} +
+ + {!collapsed && {step.nameShort}} + + {!collapsed && checkpointStatus && checkpointStatus !== 'pending' && ( +
+ {checkpointStatus === 'passed' ? ( +
+ +
+ ) : checkpointStatus === 'failed' ? ( +
+ ! +
+ ) : ( +
+ +
+ )} +
+ )} +
+ ) + + if (isLocked) return content + + return ( + + {content} + + ) +} + +// ============================================================================= +// 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 ( + + {icon} + {!collapsed && ( + + {label} + + + + + )} + + ) + } + + return ( + + {icon} + {!collapsed && {label}} + + ) +} + +// ============================================================================= +// 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 ( +
+
+
30 ? 'bg-amber-400' : 'bg-green-400'}`} /> + RAG Corpus: {totalChunks} Chunks +
+ {daysSinceUpdate > 30 && ( +
+ Corpus {daysSinceUpdate}d alt — Re-Evaluation empfohlen +
+ )} +
+ ) +} diff --git a/admin-compliance/components/sdk/compliance-scope/DatenkategorienBlock.tsx b/admin-compliance/components/sdk/compliance-scope/DatenkategorienBlock.tsx new file mode 100644 index 0000000..03e5460 --- /dev/null +++ b/admin-compliance/components/sdk/compliance-scope/DatenkategorienBlock.tsx @@ -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 = { + 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 = { + 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>(new Set()) + const [initializedDepts, setInitializedDepts] = useState>(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 ( +
+

+ Bitte waehlen Sie zuerst in Block 8 (Verarbeitungstaetigkeiten) die + Abteilungen aus, in denen personenbezogene Daten verarbeitet werden. +

+
+ ) + } + + return ( +
+ {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 ( +
+ {/* Header */} + + + {/* Expandable categories panel */} + {isExpanded && ( +
+

+ Datenkategorien +

+
+ {config.categories.map(cat => { + const isChecked = selectedCategories.includes(cat.id) + return ( + + ) + })} +
+ + {/* Art. 9 warning */} + {hasArt9Selected && ( +
+

+ Art. 9 DSGVO: 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). +

+
+ )} +
+ )} +
+ ) + })} +
+ ) +} diff --git a/admin-compliance/components/sdk/compliance-scope/ScopeQuestionRenderer.tsx b/admin-compliance/components/sdk/compliance-scope/ScopeQuestionRenderer.tsx new file mode 100644 index 0000000..575a945 --- /dev/null +++ b/admin-compliance/components/sdk/compliance-scope/ScopeQuestionRenderer.tsx @@ -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 + onToggleHelp: (questionId: string) => void +} + +export function QuestionHelpText({ question, expandedHelp, onToggleHelp }: HelpTextProps) { + if (!question.helpText) return null + return ( + <> + + {expandedHelp.has(question.id) && ( +
+ + + + {question.helpText} +
+ )} + + ) +} + +// ============================================================================= +// QUESTION RENDERER +// ============================================================================= + +interface ScopeQuestionRendererProps { + question: ScopeProfilingQuestion + answers: ScopeProfilingAnswer[] + prefilledIds: Set + expandedHelp: Set + 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 = ( +
+ {question.question} + {question.required && *} + {isPrefilled && ( + + Aus Profil + + )} + +
+ ) + + switch (question.type) { + case 'boolean': + return ( +
+
{labelRow}
+
+ {([true, false] as const).map(val => ( + + ))} +
+
+ ) + + case 'single': + return ( +
+ {labelRow} +
+ {question.options?.map((option) => ( + + ))} +
+
+ ) + + case 'multi': { + const selectedValues = Array.isArray(currentValue) ? currentValue as string[] : [] + return ( +
+ {labelRow} +
+ {question.options?.map((option) => { + const isChecked = selectedValues.includes(option.value) + return ( + + ) + })} +
+
+ ) + } + + case 'number': + return ( +
+ {labelRow} + 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" + /> +
+ ) + + case 'text': + return ( +
+ {labelRow} + 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" + /> +
+ ) + + default: + return null + } +} diff --git a/admin-compliance/components/sdk/compliance-scope/ScopeWizardTab.tsx b/admin-compliance/components/sdk/compliance-scope/ScopeWizardTab.tsx index da53877..35457ef 100644 --- a/admin-compliance/components/sdk/compliance-scope/ScopeWizardTab.tsx +++ b/admin-compliance/components/sdk/compliance-scope/ScopeWizardTab.tsx @@ -1,10 +1,11 @@ 'use client' -import React, { useState, useCallback, useEffect, useMemo } from 'react' -import type { ScopeProfilingAnswer, ScopeProfilingQuestion } from '@/lib/sdk/compliance-scope-types' -import { SCOPE_QUESTION_BLOCKS, getBlockProgress, getTotalProgress, getAnswerValue, prefillFromCompanyProfile, getProfileInfoForBlock, getAutoFilledScoringAnswers, getUnansweredRequiredQuestions } from '@/lib/sdk/compliance-scope-profiling' -import { DEPARTMENT_DATA_CATEGORIES } from '@/lib/sdk/vvt-profiling' +import React, { useState, useCallback, useEffect } from 'react' +import type { ScopeProfilingAnswer } from '@/lib/sdk/compliance-scope-types' +import { SCOPE_QUESTION_BLOCKS, getBlockProgress, getTotalProgress, prefillFromCompanyProfile, getProfileInfoForBlock, getAutoFilledScoringAnswers, getUnansweredRequiredQuestions } from '@/lib/sdk/compliance-scope-profiling' import type { ScopeQuestionBlockId } from '@/lib/sdk/compliance-scope-types' import { useSDK } from '@/lib/sdk' +import { DatenkategorienBlock9 } from './DatenkategorienBlock' +import { ScopeQuestionRenderer } from './ScopeQuestionRenderer' interface ScopeWizardTabProps { answers: ScopeProfilingAnswer[] @@ -28,18 +29,15 @@ export function ScopeWizardTab({ const currentBlock = SCOPE_QUESTION_BLOCKS[currentBlockIndex] const totalProgress = getTotalProgress(answers) - // Load companyProfile from SDK context const { state: sdkState } = useSDK() const companyProfile = sdkState.companyProfile - // Track which question IDs were prefilled from profile const [prefilledIds, setPrefilledIds] = useState>(new Set()) // Auto-prefill from company profile on mount if answers are empty useEffect(() => { if (companyProfile && answers.length === 0) { const prefilled = prefillFromCompanyProfile(companyProfile) - // Also inject auto-filled scoring answers for questions removed from UI const autoFilled = getAutoFilledScoringAnswers(companyProfile) const allPrefilled = [...prefilled, ...autoFilled] if (allPrefilled.length > 0) { @@ -47,7 +45,6 @@ export function ScopeWizardTab({ setPrefilledIds(new Set(allPrefilled.map(a => a.questionId))) } } - // Only run on mount // eslint-disable-next-line react-hooks/exhaustive-deps }, []) @@ -61,7 +58,6 @@ export function ScopeWizardTab({ } else { onAnswersChange([...answers, { questionId, value }]) } - // Remove from prefilled set when user manually changes if (prefilledIds.has(questionId)) { setPrefilledIds(prev => { const next = new Set(prev) @@ -78,7 +74,6 @@ export function ScopeWizardTab({ const prefilled = prefillFromCompanyProfile(companyProfile) const autoFilled = getAutoFilledScoringAnswers(companyProfile) const allPrefilled = [...prefilled, ...autoFilled] - // Merge with existing answers: prefilled values for questions not yet answered const existingIds = new Set(answers.map(a => a.questionId)) const newAnswers = [...answers] const newPrefilledIds = new Set(prefilledIds) @@ -101,242 +96,18 @@ export function ScopeWizardTab({ }, [currentBlockIndex, canEvaluate, onEvaluate]) const handleBack = useCallback(() => { - if (currentBlockIndex > 0) { - setCurrentBlockIndex(currentBlockIndex - 1) - } + if (currentBlockIndex > 0) setCurrentBlockIndex(currentBlockIndex - 1) }, [currentBlockIndex]) const toggleHelp = useCallback((questionId: string) => { setExpandedHelp(prev => { const next = new Set(prev) - if (next.has(questionId)) { - next.delete(questionId) - } else { - next.add(questionId) - } + if (next.has(questionId)) next.delete(questionId) + else next.add(questionId) return next }) }, []) - // Check if a question was prefilled from company profile - const isPrefilledFromProfile = useCallback((questionId: string) => { - return prefilledIds.has(questionId) - }, [prefilledIds]) - - const renderHelpText = (question: ScopeProfilingQuestion) => { - if (!question.helpText) return null - - return ( - <> - - {expandedHelp.has(question.id) && ( -
- - - - {question.helpText} -
- )} - - ) - } - - const renderPrefilledBadge = (questionId: string) => { - if (!isPrefilledFromProfile(questionId)) return null - return ( - - Aus Profil - - ) - } - - const renderQuestion = (question: ScopeProfilingQuestion) => { - const currentValue = getAnswerValue(answers, question.id) - - switch (question.type) { - case 'boolean': - return ( -
-
-
- - {question.question} - - {question.required && *} - {renderPrefilledBadge(question.id)} - {renderHelpText(question)} -
-
-
- - -
-
- ) - - case 'single': - return ( -
-
- - {question.question} - - {question.required && *} - {renderPrefilledBadge(question.id)} - {renderHelpText(question)} -
-
- {question.options?.map((option) => ( - - ))} -
-
- ) - - case 'multi': - return ( -
-
- - {question.question} - - {question.required && *} - {renderPrefilledBadge(question.id)} - {renderHelpText(question)} -
-
- {question.options?.map((option) => { - const selectedValues = Array.isArray(currentValue) ? currentValue as string[] : [] - const isChecked = selectedValues.includes(option.value) - return ( - - ) - })} -
-
- ) - - case 'number': - return ( -
-
- - {question.question} - - {question.required && *} - {renderPrefilledBadge(question.id)} - {renderHelpText(question)} -
- 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" - /> -
- ) - - case 'text': - return ( -
-
- - {question.question} - - {question.required && *} - {renderPrefilledBadge(question.id)} - {renderHelpText(question)} -
- 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" - /> -
- ) - - default: - return null - } - } - return (
{/* Left Sidebar - Block Navigation */} @@ -350,7 +121,6 @@ export function ScopeWizardTab({ const unanswered = getUnansweredRequiredQuestions(answers, block.id) const hasRequired = block.questions.some(q => q.required) const allRequiredDone = hasRequired && unanswered.length === 0 - // For optional-only blocks: check if any questions were answered const answeredIds = new Set(answers.map(a => a.questionId)) const hasAnyAnswer = block.questions.some(q => answeredIds.has(q.id)) const optionalDone = !hasRequired && hasAnyAnswer @@ -380,19 +150,13 @@ export function ScopeWizardTab({ ) : !hasRequired ? ( (nur optional) ) : ( - - {unanswered.length} offen - + {unanswered.length} offen )}
@@ -428,8 +192,6 @@ export function ScopeWizardTab({ {(() => { const allUnanswered = getUnansweredRequiredQuestions(answers) if (allUnanswered.length === 0) return null - - // Group by block const byBlock = new Map() for (const item of allUnanswered) { if (!byBlock.has(item.blockId)) { @@ -438,7 +200,6 @@ export function ScopeWizardTab({ } byBlock.get(item.blockId)!.count++ } - return (
⚠ Offene Pflichtfragen: @@ -477,7 +238,7 @@ export function ScopeWizardTab({ )}
- {/* "Aus Profil" Info Box — shown for blocks that have auto-filled data */} + {/* "Aus Profil" Info Box */} {companyProfile && (() => { const profileItems = getProfileInfoForBlock(companyProfile, currentBlock.id as ScopeQuestionBlockId) if (profileItems.length === 0) return null @@ -516,21 +277,23 @@ export function ScopeWizardTab({ {/* Questions */}
{currentBlock.id === 'datenkategorien_detail' ? ( - + ) : ( currentBlock.questions.map((question) => { const isAnswered = answers.some(a => a.questionId === question.id) const borderClass = question.required - ? isAnswered - ? 'border-l-4 border-l-green-400 pl-4' - : 'border-l-4 border-l-orange-400 pl-4' + ? isAnswered ? 'border-l-4 border-l-green-400 pl-4' : 'border-l-4 border-l-orange-400 pl-4' : '' return (
- {renderQuestion(question)} +
) }) @@ -574,221 +337,3 @@ export function ScopeWizardTab({
) } - -// ============================================================================= -// BLOCK 9: Datenkategorien pro Abteilung (aufklappbare Kacheln) -// ============================================================================= - -/** Mapping Block 8 vvt_departments values → DEPARTMENT_DATA_CATEGORIES keys */ -const DEPT_VALUE_TO_KEY: Record = { - 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 = { - 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>(new Set()) - const [initializedDepts, setInitializedDepts] = useState>(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 ( -
-

- Bitte waehlen Sie zuerst in Block 8 (Verarbeitungstaetigkeiten) die - Abteilungen aus, in denen personenbezogene Daten verarbeitet werden. -

-
- ) - } - - return ( -
- {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 ( -
- {/* Header */} - - - {/* Expandable categories panel */} - {isExpanded && ( -
-

- Datenkategorien -

-
- {config.categories.map(cat => { - const isChecked = selectedCategories.includes(cat.id) - return ( - - ) - })} -
- - {/* Art. 9 warning */} - {hasArt9Selected && ( -
-

- Art. 9 DSGVO: 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). -

-
- )} -
- )} -
- ) - })} -
- ) -}