fix(sdk): Fix compliance scope wizard — missing labels, broken prefill, invisible helpText

- Rename `label` to `question` in profiling data (35 questions) to match
  ScopeProfilingQuestion type — fixes missing question headings
- Sync ScopeWizardTab props with page.tsx (onEvaluate/canEvaluate/isEvaluating
  instead of onComplete/companyProfile/currentLevel)
- Load companyProfile from SDK context instead of expecting it as prop
- Auto-prefill from company profile on mount when answers are empty
- Add "Aus Profil" badge for prefilled questions
- Replace title-only helpText tooltip with click-to-expand visible info box
- Fix ScopeQuestionBlockId to match actual block IDs in data
- Add `order` field to ScopeQuestionBlock type
- Fix completionStats to count against total required questions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-02-10 13:42:31 +01:00
parent 34f3dbdfc3
commit 9ec5a88af9
4 changed files with 243 additions and 193 deletions

View File

@@ -1,57 +1,97 @@
'use client'
import React, { useState, useCallback } from 'react'
import type { ScopeProfilingAnswer, ComplianceDepthLevel } from '@/lib/sdk/compliance-scope-types'
import { SCOPE_QUESTION_BLOCKS, getBlockProgress, getTotalProgress, getAnswerValue, getAllQuestions } from '@/lib/sdk/compliance-scope-profiling'
import type { CompanyProfile } from '@/lib/sdk/types'
import { prefillFromCompanyProfile } from '@/lib/sdk/compliance-scope-profiling'
import { DEPTH_LEVEL_LABELS, DEPTH_LEVEL_COLORS } from '@/lib/sdk/compliance-scope-types'
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 } from '@/lib/sdk/compliance-scope-profiling'
import { useSDK } from '@/lib/sdk'
interface ScopeWizardTabProps {
answers: ScopeProfilingAnswer[]
onAnswersChange: (answers: ScopeProfilingAnswer[]) => void
onComplete: () => void
companyProfile: CompanyProfile | null
currentLevel: ComplianceDepthLevel | null
onEvaluate: () => void
canEvaluate: boolean
isEvaluating: boolean
completionStats: { total: number; answered: number; percentage: number; isComplete: boolean }
}
export function ScopeWizardTab({
answers,
onAnswersChange,
onComplete,
companyProfile,
currentLevel,
onEvaluate,
canEvaluate,
isEvaluating,
completionStats,
}: ScopeWizardTabProps) {
const [currentBlockIndex, setCurrentBlockIndex] = useState(0)
const [expandedHelp, setExpandedHelp] = useState<Set<string>>(new Set())
const currentBlock = SCOPE_QUESTION_BLOCKS[currentBlockIndex]
const totalProgress = getTotalProgress(answers)
// Load companyProfile from SDK context
const { state: sdkState } = useSDK()
const companyProfile = sdkState.companyProfile
// Track which question IDs were prefilled from profile
const [prefilledIds, setPrefilledIds] = useState<Set<string>>(new Set())
// Auto-prefill from company profile on mount if answers are empty
useEffect(() => {
if (companyProfile && answers.length === 0) {
const prefilled = prefillFromCompanyProfile(companyProfile)
if (prefilled.length > 0) {
onAnswersChange(prefilled)
setPrefilledIds(new Set(prefilled.map(a => a.questionId)))
}
}
// Only run on mount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const handleAnswerChange = useCallback(
(questionId: string, value: any) => {
(questionId: string, value: string | string[] | boolean | number) => {
const existingIndex = answers.findIndex((a) => a.questionId === questionId)
if (existingIndex >= 0) {
const newAnswers = [...answers]
newAnswers[existingIndex] = { questionId, value, answeredAt: new Date().toISOString() }
newAnswers[existingIndex] = { questionId, value }
onAnswersChange(newAnswers)
} else {
onAnswersChange([...answers, { questionId, value, answeredAt: new Date().toISOString() }])
onAnswersChange([...answers, { questionId, value }])
}
// Remove from prefilled set when user manually changes
if (prefilledIds.has(questionId)) {
setPrefilledIds(prev => {
const next = new Set(prev)
next.delete(questionId)
return next
})
}
},
[answers, onAnswersChange]
[answers, onAnswersChange, prefilledIds]
)
const handlePrefillFromProfile = useCallback(() => {
if (!companyProfile) return
const prefilledAnswers = prefillFromCompanyProfile(companyProfile, answers)
onAnswersChange(prefilledAnswers)
}, [companyProfile, answers, onAnswersChange])
const prefilled = prefillFromCompanyProfile(companyProfile)
// 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)
for (const pa of prefilled) {
if (!existingIds.has(pa.questionId)) {
newAnswers.push(pa)
newPrefilledIds.add(pa.questionId)
}
}
onAnswersChange(newAnswers)
setPrefilledIds(newPrefilledIds)
}, [companyProfile, answers, onAnswersChange, prefilledIds])
const handleNext = useCallback(() => {
if (currentBlockIndex < SCOPE_QUESTION_BLOCKS.length - 1) {
setCurrentBlockIndex(currentBlockIndex + 1)
} else {
onComplete()
} else if (canEvaluate) {
onEvaluate()
}
}, [currentBlockIndex, onComplete])
}, [currentBlockIndex, canEvaluate, onEvaluate])
const handleBack = useCallback(() => {
if (currentBlockIndex > 0) {
@@ -59,34 +99,87 @@ export function ScopeWizardTab({
}
}, [currentBlockIndex])
const renderQuestion = (question: any) => {
const toggleHelp = useCallback((questionId: string) => {
setExpandedHelp(prev => {
const next = new Set(prev)
if (next.has(questionId)) {
next.delete(questionId)
} else {
next.add(questionId)
}
return next
})
}, [])
// Check if a question was prefilled from company profile
const isPrefilledFromProfile = useCallback((questionId: string) => {
return prefilledIds.has(questionId)
}, [prefilledIds])
const renderHelpText = (question: ScopeProfilingQuestion) => {
if (!question.helpText) return null
return (
<>
<button
type="button"
className="ml-2 text-blue-400 hover:text-blue-600 inline-flex items-center"
onClick={(e) => {
e.preventDefault()
toggleHelp(question.id)
}}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</button>
{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-center justify-between">
<label className="text-sm font-medium text-gray-900">
{question.question}
<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>}
</label>
{question.helpText && (
<button
type="button"
className="text-gray-400 hover:text-gray-600"
title={question.helpText}
>
<svg className="w-5 h-5" 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>
)}
{renderPrefilledBadge(question.id)}
{renderHelpText(question)}
</div>
</div>
<div className="flex gap-3">
<button
@@ -118,28 +211,16 @@ export function ScopeWizardTab({
case 'single':
return (
<div className="space-y-2">
<label className="text-sm font-medium text-gray-900">
{question.question}
<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>}
{question.helpText && (
<button
type="button"
className="ml-2 text-gray-400 hover:text-gray-600 inline"
title={question.helpText}
>
<svg className="w-4 h-4 inline" 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>
)}
</label>
{renderPrefilledBadge(question.id)}
{renderHelpText(question)}
</div>
<div className="space-y-2">
{question.options?.map((option: any) => (
{question.options?.map((option) => (
<button
key={option.value}
type="button"
@@ -160,29 +241,17 @@ export function ScopeWizardTab({
case 'multi':
return (
<div className="space-y-2">
<label className="text-sm font-medium text-gray-900">
{question.question}
<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>}
{question.helpText && (
<button
type="button"
className="ml-2 text-gray-400 hover:text-gray-600 inline"
title={question.helpText}
>
<svg className="w-4 h-4 inline" 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>
)}
</label>
{renderPrefilledBadge(question.id)}
{renderHelpText(question)}
</div>
<div className="space-y-2">
{question.options?.map((option: any) => {
const selectedValues = Array.isArray(currentValue) ? currentValue : []
{question.options?.map((option) => {
const selectedValues = Array.isArray(currentValue) ? currentValue as string[] : []
const isChecked = selectedValues.includes(option.value)
return (
<label
@@ -217,29 +286,17 @@ export function ScopeWizardTab({
case 'number':
return (
<div className="space-y-2">
<label className="text-sm font-medium text-gray-900">
{question.question}
<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>}
{question.helpText && (
<button
type="button"
className="ml-2 text-gray-400 hover:text-gray-600 inline"
title={question.helpText}
>
<svg className="w-4 h-4 inline" 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>
)}
</label>
{renderPrefilledBadge(question.id)}
{renderHelpText(question)}
</div>
<input
type="number"
value={currentValue ?? ''}
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"
@@ -250,29 +307,17 @@ export function ScopeWizardTab({
case 'text':
return (
<div className="space-y-2">
<label className="text-sm font-medium text-gray-900">
{question.question}
<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>}
{question.helpText && (
<button
type="button"
className="ml-2 text-gray-400 hover:text-gray-600 inline"
title={question.helpText}
>
<svg className="w-4 h-4 inline" 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>
)}
</label>
{renderPrefilledBadge(question.id)}
{renderHelpText(question)}
</div>
<input
type="text"
value={currentValue ?? ''}
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"
@@ -334,16 +379,9 @@ export function ScopeWizardTab({
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-gray-700">Gesamtfortschritt</span>
<div className="flex items-center gap-3">
{currentLevel && (
<div className="flex items-center gap-2">
<span className="text-xs text-gray-500">Vorläufige Einstufung:</span>
<span
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-bold ${DEPTH_LEVEL_COLORS[currentLevel].badge} ${DEPTH_LEVEL_COLORS[currentLevel].text}`}
>
{currentLevel} - {DEPTH_LEVEL_LABELS[currentLevel]}
</span>
</div>
)}
<span className="text-sm text-gray-500">
{completionStats.answered} / {completionStats.total} Fragen
</span>
<span className="text-sm font-bold text-gray-900">{totalProgress}%</span>
</div>
</div>
@@ -368,7 +406,7 @@ export function ScopeWizardTab({
onClick={handlePrefillFromProfile}
className="px-4 py-2 text-sm bg-blue-50 text-blue-700 border border-blue-300 rounded-lg hover:bg-blue-100 transition-colors whitespace-nowrap"
>
Aus Unternehmensprofil übernehmen
Aus Profil uebernehmen
</button>
)}
</div>
@@ -391,18 +429,29 @@ export function ScopeWizardTab({
disabled={currentBlockIndex === 0}
className="px-6 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
Zurück
Zurueck
</button>
<span className="text-sm text-gray-600">
Block {currentBlockIndex + 1} von {SCOPE_QUESTION_BLOCKS.length}
</span>
<button
type="button"
onClick={handleNext}
className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors font-medium"
>
{currentBlockIndex === SCOPE_QUESTION_BLOCKS.length - 1 ? 'Auswertung starten' : 'Weiter'}
</button>
{currentBlockIndex === SCOPE_QUESTION_BLOCKS.length - 1 ? (
<button
type="button"
onClick={onEvaluate}
disabled={!canEvaluate || isEvaluating}
className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed"
>
{isEvaluating ? 'Evaluiere...' : 'Auswertung starten'}
</button>
) : (
<button
type="button"
onClick={handleNext}
className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors font-medium"
>
Weiter
</button>
)}
</div>
</div>
</div>