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>
196 lines
7.0 KiB
TypeScript
196 lines
7.0 KiB
TypeScript
'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
|
|
}
|
|
}
|