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 81536d9738
commit b3e9604d72
4 changed files with 243 additions and 193 deletions

View File

@@ -19,6 +19,7 @@ import {
STORAGE_KEY STORAGE_KEY
} from '@/lib/sdk/compliance-scope-types' } from '@/lib/sdk/compliance-scope-types'
import { complianceScopeEngine } from '@/lib/sdk/compliance-scope-engine' import { complianceScopeEngine } from '@/lib/sdk/compliance-scope-engine'
import { getAllQuestions } from '@/lib/sdk/compliance-scope-profiling'
type TabId = 'overview' | 'wizard' | 'decision' | 'export' type TabId = 'overview' | 'wizard' | 'decision' | 'export'
@@ -78,7 +79,7 @@ export default function ComplianceScopePage() {
}, [scopeState, isLoading, dispatch]) }, [scopeState, isLoading, dispatch])
// Handle answers change from wizard // Handle answers change from wizard
const handleAnswersChange = useCallback((answers: Record<string, ScopeProfilingAnswer>) => { const handleAnswersChange = useCallback((answers: ScopeProfilingAnswer[]) => {
setScopeState(prev => ({ setScopeState(prev => ({
...prev, ...prev,
answers, answers,
@@ -125,11 +126,11 @@ export default function ComplianceScopePage() {
// Calculate completion statistics // Calculate completion statistics
const completionStats = useMemo(() => { const completionStats = useMemo(() => {
const answers = scopeState.answers const allQuestions = getAllQuestions()
const totalQuestions = Object.keys(answers).length const requiredQuestions = allQuestions.filter(q => q.required)
const answeredQuestions = Object.values(answers).filter( const totalQuestions = requiredQuestions.length
answer => answer.value !== null && answer.value !== undefined const answeredIds = new Set(scopeState.answers.map(a => a.questionId))
).length const answeredQuestions = requiredQuestions.filter(q => answeredIds.has(q.id)).length
const completionPercentage = totalQuestions > 0 const completionPercentage = totalQuestions > 0
? Math.round((answeredQuestions / totalQuestions) * 100) ? Math.round((answeredQuestions / totalQuestions) * 100)
@@ -350,7 +351,7 @@ export default function ComplianceScopePage() {
<span className="font-semibold">Active Tab:</span> {activeTab} <span className="font-semibold">Active Tab:</span> {activeTab}
</div> </div>
<div> <div>
<span className="font-semibold">Total Answers:</span> {Object.keys(scopeState.answers).length} <span className="font-semibold">Total Answers:</span> {scopeState.answers.length}
</div> </div>
<div> <div>
<span className="font-semibold">Answered:</span> {completionStats.answered} ({completionStats.percentage}%) <span className="font-semibold">Answered:</span> {completionStats.answered} ({completionStats.percentage}%)

View File

@@ -1,57 +1,97 @@
'use client' 'use client'
import React, { useState, useCallback } from 'react' import React, { useState, useCallback, useEffect, useMemo } from 'react'
import type { ScopeProfilingAnswer, ComplianceDepthLevel } from '@/lib/sdk/compliance-scope-types' import type { ScopeProfilingAnswer, ScopeProfilingQuestion } from '@/lib/sdk/compliance-scope-types'
import { SCOPE_QUESTION_BLOCKS, getBlockProgress, getTotalProgress, getAnswerValue, getAllQuestions } from '@/lib/sdk/compliance-scope-profiling' import { SCOPE_QUESTION_BLOCKS, getBlockProgress, getTotalProgress, getAnswerValue, prefillFromCompanyProfile } from '@/lib/sdk/compliance-scope-profiling'
import type { CompanyProfile } from '@/lib/sdk/types' import { useSDK } from '@/lib/sdk'
import { prefillFromCompanyProfile } from '@/lib/sdk/compliance-scope-profiling'
import { DEPTH_LEVEL_LABELS, DEPTH_LEVEL_COLORS } from '@/lib/sdk/compliance-scope-types'
interface ScopeWizardTabProps { interface ScopeWizardTabProps {
answers: ScopeProfilingAnswer[] answers: ScopeProfilingAnswer[]
onAnswersChange: (answers: ScopeProfilingAnswer[]) => void onAnswersChange: (answers: ScopeProfilingAnswer[]) => void
onComplete: () => void onEvaluate: () => void
companyProfile: CompanyProfile | null canEvaluate: boolean
currentLevel: ComplianceDepthLevel | null isEvaluating: boolean
completionStats: { total: number; answered: number; percentage: number; isComplete: boolean }
} }
export function ScopeWizardTab({ export function ScopeWizardTab({
answers, answers,
onAnswersChange, onAnswersChange,
onComplete, onEvaluate,
companyProfile, canEvaluate,
currentLevel, isEvaluating,
completionStats,
}: ScopeWizardTabProps) { }: ScopeWizardTabProps) {
const [currentBlockIndex, setCurrentBlockIndex] = useState(0) const [currentBlockIndex, setCurrentBlockIndex] = useState(0)
const [expandedHelp, setExpandedHelp] = useState<Set<string>>(new Set())
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 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( const handleAnswerChange = useCallback(
(questionId: string, value: any) => { (questionId: string, value: string | string[] | boolean | number) => {
const existingIndex = answers.findIndex((a) => a.questionId === questionId) const existingIndex = answers.findIndex((a) => a.questionId === questionId)
if (existingIndex >= 0) { if (existingIndex >= 0) {
const newAnswers = [...answers] const newAnswers = [...answers]
newAnswers[existingIndex] = { questionId, value, answeredAt: new Date().toISOString() } newAnswers[existingIndex] = { questionId, value }
onAnswersChange(newAnswers) onAnswersChange(newAnswers)
} else { } 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(() => { const handlePrefillFromProfile = useCallback(() => {
if (!companyProfile) return if (!companyProfile) return
const prefilledAnswers = prefillFromCompanyProfile(companyProfile, answers) const prefilled = prefillFromCompanyProfile(companyProfile)
onAnswersChange(prefilledAnswers) // Merge with existing answers: prefilled values for questions not yet answered
}, [companyProfile, answers, onAnswersChange]) 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(() => { const handleNext = useCallback(() => {
if (currentBlockIndex < SCOPE_QUESTION_BLOCKS.length - 1) { if (currentBlockIndex < SCOPE_QUESTION_BLOCKS.length - 1) {
setCurrentBlockIndex(currentBlockIndex + 1) setCurrentBlockIndex(currentBlockIndex + 1)
} else { } else if (canEvaluate) {
onComplete() onEvaluate()
} }
}, [currentBlockIndex, onComplete]) }, [currentBlockIndex, canEvaluate, onEvaluate])
const handleBack = useCallback(() => { const handleBack = useCallback(() => {
if (currentBlockIndex > 0) { if (currentBlockIndex > 0) {
@@ -59,34 +99,87 @@ export function ScopeWizardTab({
} }
}, [currentBlockIndex]) }, [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) const currentValue = getAnswerValue(answers, question.id)
switch (question.type) { switch (question.type) {
case 'boolean': case 'boolean':
return ( return (
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center justify-between"> <div className="flex items-start justify-between">
<label className="text-sm font-medium text-gray-900"> <div className="flex items-center flex-wrap gap-1">
{question.question} <span className="text-sm font-medium text-gray-900">
{question.question}
</span>
{question.required && <span className="text-red-500 ml-1">*</span>} {question.required && <span className="text-red-500 ml-1">*</span>}
</label> {renderPrefilledBadge(question.id)}
{question.helpText && ( {renderHelpText(question)}
<button </div>
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>
)}
</div> </div>
<div className="flex gap-3"> <div className="flex gap-3">
<button <button
@@ -118,28 +211,16 @@ export function ScopeWizardTab({
case 'single': case 'single':
return ( return (
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium text-gray-900"> <div className="flex items-center flex-wrap gap-1">
{question.question} <span className="text-sm font-medium text-gray-900">
{question.question}
</span>
{question.required && <span className="text-red-500 ml-1">*</span>} {question.required && <span className="text-red-500 ml-1">*</span>}
{question.helpText && ( {renderPrefilledBadge(question.id)}
<button {renderHelpText(question)}
type="button" </div>
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>
<div className="space-y-2"> <div className="space-y-2">
{question.options?.map((option: any) => ( {question.options?.map((option) => (
<button <button
key={option.value} key={option.value}
type="button" type="button"
@@ -160,29 +241,17 @@ export function ScopeWizardTab({
case 'multi': case 'multi':
return ( return (
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium text-gray-900"> <div className="flex items-center flex-wrap gap-1">
{question.question} <span className="text-sm font-medium text-gray-900">
{question.question}
</span>
{question.required && <span className="text-red-500 ml-1">*</span>} {question.required && <span className="text-red-500 ml-1">*</span>}
{question.helpText && ( {renderPrefilledBadge(question.id)}
<button {renderHelpText(question)}
type="button" </div>
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>
<div className="space-y-2"> <div className="space-y-2">
{question.options?.map((option: any) => { {question.options?.map((option) => {
const selectedValues = Array.isArray(currentValue) ? currentValue : [] const selectedValues = Array.isArray(currentValue) ? currentValue as string[] : []
const isChecked = selectedValues.includes(option.value) const isChecked = selectedValues.includes(option.value)
return ( return (
<label <label
@@ -217,29 +286,17 @@ export function ScopeWizardTab({
case 'number': case 'number':
return ( return (
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium text-gray-900"> <div className="flex items-center flex-wrap gap-1">
{question.question} <span className="text-sm font-medium text-gray-900">
{question.question}
</span>
{question.required && <span className="text-red-500 ml-1">*</span>} {question.required && <span className="text-red-500 ml-1">*</span>}
{question.helpText && ( {renderPrefilledBadge(question.id)}
<button {renderHelpText(question)}
type="button" </div>
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>
<input <input
type="number" type="number"
value={currentValue ?? ''} value={currentValue != null ? String(currentValue) : ''}
onChange={(e) => handleAnswerChange(question.id, parseInt(e.target.value, 10))} 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" 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" placeholder="Zahl eingeben"
@@ -250,29 +307,17 @@ export function ScopeWizardTab({
case 'text': case 'text':
return ( return (
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium text-gray-900"> <div className="flex items-center flex-wrap gap-1">
{question.question} <span className="text-sm font-medium text-gray-900">
{question.question}
</span>
{question.required && <span className="text-red-500 ml-1">*</span>} {question.required && <span className="text-red-500 ml-1">*</span>}
{question.helpText && ( {renderPrefilledBadge(question.id)}
<button {renderHelpText(question)}
type="button" </div>
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>
<input <input
type="text" type="text"
value={currentValue ?? ''} value={currentValue != null ? String(currentValue) : ''}
onChange={(e) => handleAnswerChange(question.id, e.target.value)} 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" 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" placeholder="Text eingeben"
@@ -334,16 +379,9 @@ export function ScopeWizardTab({
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-gray-700">Gesamtfortschritt</span> <span className="text-sm font-medium text-gray-700">Gesamtfortschritt</span>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{currentLevel && ( <span className="text-sm text-gray-500">
<div className="flex items-center gap-2"> {completionStats.answered} / {completionStats.total} Fragen
<span className="text-xs text-gray-500">Vorläufige Einstufung:</span> </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 font-bold text-gray-900">{totalProgress}%</span> <span className="text-sm font-bold text-gray-900">{totalProgress}%</span>
</div> </div>
</div> </div>
@@ -368,7 +406,7 @@ export function ScopeWizardTab({
onClick={handlePrefillFromProfile} 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" 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> </button>
)} )}
</div> </div>
@@ -391,18 +429,29 @@ export function ScopeWizardTab({
disabled={currentBlockIndex === 0} 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" 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> </button>
<span className="text-sm text-gray-600"> <span className="text-sm text-gray-600">
Block {currentBlockIndex + 1} von {SCOPE_QUESTION_BLOCKS.length} Block {currentBlockIndex + 1} von {SCOPE_QUESTION_BLOCKS.length}
</span> </span>
<button {currentBlockIndex === SCOPE_QUESTION_BLOCKS.length - 1 ? (
type="button" <button
onClick={handleNext} type="button"
className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors font-medium" onClick={onEvaluate}
> disabled={!canEvaluate || isEvaluating}
{currentBlockIndex === SCOPE_QUESTION_BLOCKS.length - 1 ? 'Auswertung starten' : 'Weiter'} className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed"
</button> >
{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> </div>
</div> </div>

View File

@@ -19,7 +19,7 @@ const BLOCK_1_ORGANISATION: ScopeQuestionBlock = {
{ {
id: 'org_employee_count', id: 'org_employee_count',
type: 'number', type: 'number',
label: 'Wie viele Mitarbeiter hat Ihre Organisation?', question: 'Wie viele Mitarbeiter hat Ihre Organisation?',
helpText: 'Geben Sie die Gesamtzahl aller Beschäftigten an (inkl. Teilzeit, Minijobs)', helpText: 'Geben Sie die Gesamtzahl aller Beschäftigten an (inkl. Teilzeit, Minijobs)',
required: true, required: true,
scoreWeights: { risk: 5, complexity: 8, assurance: 6 }, scoreWeights: { risk: 5, complexity: 8, assurance: 6 },
@@ -28,7 +28,7 @@ const BLOCK_1_ORGANISATION: ScopeQuestionBlock = {
{ {
id: 'org_customer_count', id: 'org_customer_count',
type: 'single', type: 'single',
label: 'Wie viele Kunden/Nutzer betreuen Sie?', question: 'Wie viele Kunden/Nutzer betreuen Sie?',
helpText: 'Schätzen Sie die Anzahl aktiver Kunden oder Nutzer', helpText: 'Schätzen Sie die Anzahl aktiver Kunden oder Nutzer',
required: true, required: true,
options: [ options: [
@@ -43,7 +43,7 @@ const BLOCK_1_ORGANISATION: ScopeQuestionBlock = {
{ {
id: 'org_annual_revenue', id: 'org_annual_revenue',
type: 'single', type: 'single',
label: 'Wie hoch ist Ihr jährlicher Umsatz?', question: 'Wie hoch ist Ihr jährlicher Umsatz?',
helpText: 'Wählen Sie die zutreffende Umsatzklasse', helpText: 'Wählen Sie die zutreffende Umsatzklasse',
required: true, required: true,
options: [ options: [
@@ -58,7 +58,7 @@ const BLOCK_1_ORGANISATION: ScopeQuestionBlock = {
{ {
id: 'org_cert_target', id: 'org_cert_target',
type: 'multi', type: 'multi',
label: 'Welche Zertifizierungen streben Sie an oder besitzen Sie bereits?', question: 'Welche Zertifizierungen streben Sie an oder besitzen Sie bereits?',
helpText: 'Mehrfachauswahl möglich. Zertifizierungen erhöhen den Assurance-Bedarf', helpText: 'Mehrfachauswahl möglich. Zertifizierungen erhöhen den Assurance-Bedarf',
required: false, required: false,
options: [ options: [
@@ -74,7 +74,7 @@ const BLOCK_1_ORGANISATION: ScopeQuestionBlock = {
{ {
id: 'org_industry', id: 'org_industry',
type: 'single', type: 'single',
label: 'In welcher Branche sind Sie tätig?', question: 'In welcher Branche sind Sie tätig?',
helpText: 'Ihre Branche beeinflusst Risikobewertung und regulatorische Anforderungen', helpText: 'Ihre Branche beeinflusst Risikobewertung und regulatorische Anforderungen',
required: true, required: true,
options: [ options: [
@@ -96,7 +96,7 @@ const BLOCK_1_ORGANISATION: ScopeQuestionBlock = {
{ {
id: 'org_business_model', id: 'org_business_model',
type: 'single', type: 'single',
label: 'Was ist Ihr primäres Geschäftsmodell?', question: 'Was ist Ihr primäres Geschäftsmodell?',
helpText: 'B2C-Modelle haben höhere Datenschutzanforderungen', helpText: 'B2C-Modelle haben höhere Datenschutzanforderungen',
required: true, required: true,
options: [ options: [
@@ -113,7 +113,7 @@ const BLOCK_1_ORGANISATION: ScopeQuestionBlock = {
{ {
id: 'org_has_dsb', id: 'org_has_dsb',
type: 'boolean', type: 'boolean',
label: 'Haben Sie einen Datenschutzbeauftragten bestellt?', question: 'Haben Sie einen Datenschutzbeauftragten bestellt?',
helpText: 'Ein DSB ist bei mehr als 20 Personen mit regelmäßiger Datenverarbeitung Pflicht', helpText: 'Ein DSB ist bei mehr als 20 Personen mit regelmäßiger Datenverarbeitung Pflicht',
required: true, required: true,
scoreWeights: { risk: 5, complexity: 3, assurance: 6 }, scoreWeights: { risk: 5, complexity: 3, assurance: 6 },
@@ -133,7 +133,7 @@ const BLOCK_2_DATA: ScopeQuestionBlock = {
{ {
id: 'data_minors', id: 'data_minors',
type: 'boolean', type: 'boolean',
label: 'Verarbeiten Sie Daten von Minderjährigen?', question: 'Verarbeiten Sie Daten von Minderjährigen?',
helpText: 'Besondere Schutzpflichten für unter 16-Jährige (bzw. 13-Jährige bei Online-Diensten)', helpText: 'Besondere Schutzpflichten für unter 16-Jährige (bzw. 13-Jährige bei Online-Diensten)',
required: true, required: true,
scoreWeights: { risk: 10, complexity: 5, assurance: 7 }, scoreWeights: { risk: 10, complexity: 5, assurance: 7 },
@@ -142,7 +142,7 @@ const BLOCK_2_DATA: ScopeQuestionBlock = {
{ {
id: 'data_art9', id: 'data_art9',
type: 'multi', type: 'multi',
label: 'Verarbeiten Sie besondere Kategorien personenbezogener Daten (Art. 9 DSGVO)?', question: 'Verarbeiten Sie besondere Kategorien personenbezogener Daten (Art. 9 DSGVO)?',
helpText: 'Diese Daten unterliegen erhöhten Schutzanforderungen', helpText: 'Diese Daten unterliegen erhöhten Schutzanforderungen',
required: true, required: true,
options: [ options: [
@@ -162,7 +162,7 @@ const BLOCK_2_DATA: ScopeQuestionBlock = {
{ {
id: 'data_hr', id: 'data_hr',
type: 'boolean', type: 'boolean',
label: 'Verarbeiten Sie Personaldaten (HR)?', question: 'Verarbeiten Sie Personaldaten (HR)?',
helpText: 'Bewerberdaten, Gehälter, Leistungsbeurteilungen etc.', helpText: 'Bewerberdaten, Gehälter, Leistungsbeurteilungen etc.',
required: true, required: true,
scoreWeights: { risk: 6, complexity: 4, assurance: 5 }, scoreWeights: { risk: 6, complexity: 4, assurance: 5 },
@@ -172,7 +172,7 @@ const BLOCK_2_DATA: ScopeQuestionBlock = {
{ {
id: 'data_communication', id: 'data_communication',
type: 'boolean', type: 'boolean',
label: 'Verarbeiten Sie Kommunikationsdaten (E-Mail, Chat, Telefonie)?', question: 'Verarbeiten Sie Kommunikationsdaten (E-Mail, Chat, Telefonie)?',
helpText: 'Inhalte oder Metadaten von Kommunikationsvorgängen', helpText: 'Inhalte oder Metadaten von Kommunikationsvorgängen',
required: true, required: true,
scoreWeights: { risk: 7, complexity: 5, assurance: 6 }, scoreWeights: { risk: 7, complexity: 5, assurance: 6 },
@@ -180,7 +180,7 @@ const BLOCK_2_DATA: ScopeQuestionBlock = {
{ {
id: 'data_financial', id: 'data_financial',
type: 'boolean', type: 'boolean',
label: 'Verarbeiten Sie Finanzdaten (Konten, Zahlungen)?', question: 'Verarbeiten Sie Finanzdaten (Konten, Zahlungen)?',
helpText: 'Bankdaten, Kreditkartendaten, Buchhaltungsdaten', helpText: 'Bankdaten, Kreditkartendaten, Buchhaltungsdaten',
required: true, required: true,
scoreWeights: { risk: 8, complexity: 6, assurance: 7 }, scoreWeights: { risk: 8, complexity: 6, assurance: 7 },
@@ -190,7 +190,7 @@ const BLOCK_2_DATA: ScopeQuestionBlock = {
{ {
id: 'data_volume', id: 'data_volume',
type: 'single', type: 'single',
label: 'Wie viele Personendatensätze verarbeiten Sie insgesamt?', question: 'Wie viele Personendatensätze verarbeiten Sie insgesamt?',
helpText: 'Schätzen Sie die Gesamtzahl betroffener Personen', helpText: 'Schätzen Sie die Gesamtzahl betroffener Personen',
required: true, required: true,
options: [ options: [
@@ -217,7 +217,7 @@ const BLOCK_3_PROCESSING: ScopeQuestionBlock = {
{ {
id: 'proc_tracking', id: 'proc_tracking',
type: 'boolean', type: 'boolean',
label: 'Setzen Sie Tracking oder Profiling ein?', question: 'Setzen Sie Tracking oder Profiling ein?',
helpText: 'Web-Analytics, Werbe-Tracking, Nutzungsprofile etc.', helpText: 'Web-Analytics, Werbe-Tracking, Nutzungsprofile etc.',
required: true, required: true,
scoreWeights: { risk: 7, complexity: 6, assurance: 6 }, scoreWeights: { risk: 7, complexity: 6, assurance: 6 },
@@ -225,7 +225,7 @@ const BLOCK_3_PROCESSING: ScopeQuestionBlock = {
{ {
id: 'proc_adm_scoring', id: 'proc_adm_scoring',
type: 'boolean', type: 'boolean',
label: 'Treffen Sie automatisierte Entscheidungen (Art. 22 DSGVO)?', question: 'Treffen Sie automatisierte Entscheidungen (Art. 22 DSGVO)?',
helpText: 'Scoring, Bonitätsprüfung, automatische Ablehnung ohne menschliche Beteiligung', helpText: 'Scoring, Bonitätsprüfung, automatische Ablehnung ohne menschliche Beteiligung',
required: true, required: true,
scoreWeights: { risk: 9, complexity: 8, assurance: 8 }, scoreWeights: { risk: 9, complexity: 8, assurance: 8 },
@@ -233,7 +233,7 @@ const BLOCK_3_PROCESSING: ScopeQuestionBlock = {
{ {
id: 'proc_ai_usage', id: 'proc_ai_usage',
type: 'multi', type: 'multi',
label: 'Setzen Sie KI-Systeme ein?', question: 'Setzen Sie KI-Systeme ein?',
helpText: 'KI-Einsatz kann zusätzliche Anforderungen (EU AI Act) auslösen', helpText: 'KI-Einsatz kann zusätzliche Anforderungen (EU AI Act) auslösen',
required: true, required: true,
options: [ options: [
@@ -249,7 +249,7 @@ const BLOCK_3_PROCESSING: ScopeQuestionBlock = {
{ {
id: 'proc_data_combination', id: 'proc_data_combination',
type: 'boolean', type: 'boolean',
label: 'Führen Sie Daten aus verschiedenen Quellen zusammen?', question: 'Führen Sie Daten aus verschiedenen Quellen zusammen?',
helpText: 'Data Matching, Anreicherung aus externen Quellen', helpText: 'Data Matching, Anreicherung aus externen Quellen',
required: true, required: true,
scoreWeights: { risk: 7, complexity: 7, assurance: 6 }, scoreWeights: { risk: 7, complexity: 7, assurance: 6 },
@@ -257,7 +257,7 @@ const BLOCK_3_PROCESSING: ScopeQuestionBlock = {
{ {
id: 'proc_employee_monitoring', id: 'proc_employee_monitoring',
type: 'boolean', type: 'boolean',
label: 'Überwachen Sie Mitarbeiter (Zeiterfassung, Standort, IT-Nutzung)?', question: 'Überwachen Sie Mitarbeiter (Zeiterfassung, Standort, IT-Nutzung)?',
helpText: 'Beschäftigtendatenschutz nach § 26 BDSG', helpText: 'Beschäftigtendatenschutz nach § 26 BDSG',
required: true, required: true,
scoreWeights: { risk: 8, complexity: 6, assurance: 7 }, scoreWeights: { risk: 8, complexity: 6, assurance: 7 },
@@ -265,7 +265,7 @@ const BLOCK_3_PROCESSING: ScopeQuestionBlock = {
{ {
id: 'proc_video_surveillance', id: 'proc_video_surveillance',
type: 'boolean', type: 'boolean',
label: 'Setzen Sie Videoüberwachung ein?', question: 'Setzen Sie Videoüberwachung ein?',
helpText: 'Kameras in Büros, Produktionsstätten, Verkaufsräumen etc.', helpText: 'Kameras in Büros, Produktionsstätten, Verkaufsräumen etc.',
required: true, required: true,
scoreWeights: { risk: 8, complexity: 5, assurance: 7 }, scoreWeights: { risk: 8, complexity: 5, assurance: 7 },
@@ -287,7 +287,7 @@ const BLOCK_4_TECH: ScopeQuestionBlock = {
{ {
id: 'tech_hosting_location', id: 'tech_hosting_location',
type: 'single', type: 'single',
label: 'Wo werden Ihre Daten primär gehostet?', question: 'Wo werden Ihre Daten primär gehostet?',
helpText: 'Standort bestimmt anwendbares Datenschutzrecht', helpText: 'Standort bestimmt anwendbares Datenschutzrecht',
required: true, required: true,
options: [ options: [
@@ -302,7 +302,7 @@ const BLOCK_4_TECH: ScopeQuestionBlock = {
{ {
id: 'tech_subprocessors', id: 'tech_subprocessors',
type: 'boolean', type: 'boolean',
label: 'Nutzen Sie Auftragsverarbeiter (externe Dienstleister)?', question: 'Nutzen Sie Auftragsverarbeiter (externe Dienstleister)?',
helpText: 'Cloud-Anbieter, Hosting, E-Mail-Service, CRM etc. erfordert AVV nach Art. 28 DSGVO', helpText: 'Cloud-Anbieter, Hosting, E-Mail-Service, CRM etc. erfordert AVV nach Art. 28 DSGVO',
required: true, required: true,
scoreWeights: { risk: 6, complexity: 7, assurance: 7 }, scoreWeights: { risk: 6, complexity: 7, assurance: 7 },
@@ -310,7 +310,7 @@ const BLOCK_4_TECH: ScopeQuestionBlock = {
{ {
id: 'tech_third_country', id: 'tech_third_country',
type: 'boolean', type: 'boolean',
label: 'Übermitteln Sie Daten in Drittländer?', question: 'Übermitteln Sie Daten in Drittländer?',
helpText: 'Transfer außerhalb EU/EWR erfordert Schutzmaßnahmen (SCC, BCR etc.)', helpText: 'Transfer außerhalb EU/EWR erfordert Schutzmaßnahmen (SCC, BCR etc.)',
required: true, required: true,
scoreWeights: { risk: 9, complexity: 8, assurance: 8 }, scoreWeights: { risk: 9, complexity: 8, assurance: 8 },
@@ -319,7 +319,7 @@ const BLOCK_4_TECH: ScopeQuestionBlock = {
{ {
id: 'tech_encryption_rest', id: 'tech_encryption_rest',
type: 'boolean', type: 'boolean',
label: 'Sind Daten im Ruhezustand verschlüsselt (at rest)?', question: 'Sind Daten im Ruhezustand verschlüsselt (at rest)?',
helpText: 'Datenbank-, Dateisystem- oder Volume-Verschlüsselung', helpText: 'Datenbank-, Dateisystem- oder Volume-Verschlüsselung',
required: true, required: true,
scoreWeights: { risk: -5, complexity: 3, assurance: 7 }, scoreWeights: { risk: -5, complexity: 3, assurance: 7 },
@@ -327,7 +327,7 @@ const BLOCK_4_TECH: ScopeQuestionBlock = {
{ {
id: 'tech_encryption_transit', id: 'tech_encryption_transit',
type: 'boolean', type: 'boolean',
label: 'Sind Daten bei Übertragung verschlüsselt (in transit)?', question: 'Sind Daten bei Übertragung verschlüsselt (in transit)?',
helpText: 'TLS/SSL für alle Verbindungen', helpText: 'TLS/SSL für alle Verbindungen',
required: true, required: true,
scoreWeights: { risk: -5, complexity: 2, assurance: 7 }, scoreWeights: { risk: -5, complexity: 2, assurance: 7 },
@@ -335,7 +335,7 @@ const BLOCK_4_TECH: ScopeQuestionBlock = {
{ {
id: 'tech_cloud_providers', id: 'tech_cloud_providers',
type: 'multi', type: 'multi',
label: 'Welche Cloud-Anbieter nutzen Sie?', question: 'Welche Cloud-Anbieter nutzen Sie?',
helpText: 'Mehrfachauswahl möglich', helpText: 'Mehrfachauswahl möglich',
required: false, required: false,
options: [ options: [
@@ -365,7 +365,7 @@ const BLOCK_5_PROCESSES: ScopeQuestionBlock = {
{ {
id: 'proc_dsar_process', id: 'proc_dsar_process',
type: 'boolean', type: 'boolean',
label: 'Haben Sie einen Prozess für Betroffenenrechte (DSAR)?', question: 'Haben Sie einen Prozess für Betroffenenrechte (DSAR)?',
helpText: 'Auskunft, Löschung, Berichtigung, Widerspruch etc. Art. 15-22 DSGVO', helpText: 'Auskunft, Löschung, Berichtigung, Widerspruch etc. Art. 15-22 DSGVO',
required: true, required: true,
scoreWeights: { risk: 6, complexity: 5, assurance: 8 }, scoreWeights: { risk: 6, complexity: 5, assurance: 8 },
@@ -373,7 +373,7 @@ const BLOCK_5_PROCESSES: ScopeQuestionBlock = {
{ {
id: 'proc_deletion_concept', id: 'proc_deletion_concept',
type: 'boolean', type: 'boolean',
label: 'Haben Sie ein Löschkonzept?', question: 'Haben Sie ein Löschkonzept?',
helpText: 'Definierte Löschfristen und automatisierte Löschroutinen', helpText: 'Definierte Löschfristen und automatisierte Löschroutinen',
required: true, required: true,
scoreWeights: { risk: 7, complexity: 6, assurance: 8 }, scoreWeights: { risk: 7, complexity: 6, assurance: 8 },
@@ -381,7 +381,7 @@ const BLOCK_5_PROCESSES: ScopeQuestionBlock = {
{ {
id: 'proc_incident_response', id: 'proc_incident_response',
type: 'boolean', type: 'boolean',
label: 'Haben Sie einen Notfallplan für Datenschutzvorfälle?', question: 'Haben Sie einen Notfallplan für Datenschutzvorfälle?',
helpText: 'Incident Response Plan, 72h-Meldepflicht an Aufsichtsbehörde (Art. 33 DSGVO)', helpText: 'Incident Response Plan, 72h-Meldepflicht an Aufsichtsbehörde (Art. 33 DSGVO)',
required: true, required: true,
scoreWeights: { risk: 8, complexity: 6, assurance: 9 }, scoreWeights: { risk: 8, complexity: 6, assurance: 9 },
@@ -389,7 +389,7 @@ const BLOCK_5_PROCESSES: ScopeQuestionBlock = {
{ {
id: 'proc_regular_audits', id: 'proc_regular_audits',
type: 'boolean', type: 'boolean',
label: 'Führen Sie regelmäßige Datenschutz-Audits durch?', question: 'Führen Sie regelmäßige Datenschutz-Audits durch?',
helpText: 'Interne oder externe Prüfungen mindestens jährlich', helpText: 'Interne oder externe Prüfungen mindestens jährlich',
required: true, required: true,
scoreWeights: { risk: 5, complexity: 4, assurance: 9 }, scoreWeights: { risk: 5, complexity: 4, assurance: 9 },
@@ -397,7 +397,7 @@ const BLOCK_5_PROCESSES: ScopeQuestionBlock = {
{ {
id: 'proc_training', id: 'proc_training',
type: 'boolean', type: 'boolean',
label: 'Schulen Sie Ihre Mitarbeiter im Datenschutz?', question: 'Schulen Sie Ihre Mitarbeiter im Datenschutz?',
helpText: 'Awareness-Trainings, Onboarding, jährliche Auffrischung', helpText: 'Awareness-Trainings, Onboarding, jährliche Auffrischung',
required: true, required: true,
scoreWeights: { risk: 6, complexity: 3, assurance: 7 }, scoreWeights: { risk: 6, complexity: 3, assurance: 7 },
@@ -417,7 +417,7 @@ const BLOCK_6_PRODUCT: ScopeQuestionBlock = {
{ {
id: 'prod_type', id: 'prod_type',
type: 'multi', type: 'multi',
label: 'Welche Art von Produkten/Services bieten Sie an?', question: 'Welche Art von Produkten/Services bieten Sie an?',
helpText: 'Mehrfachauswahl möglich', helpText: 'Mehrfachauswahl möglich',
required: true, required: true,
options: [ options: [
@@ -435,7 +435,7 @@ const BLOCK_6_PRODUCT: ScopeQuestionBlock = {
{ {
id: 'prod_cookies_consent', id: 'prod_cookies_consent',
type: 'boolean', type: 'boolean',
label: 'Benötigen Sie Cookie-Consent (Tracking-Cookies)?', question: 'Benötigen Sie Cookie-Consent (Tracking-Cookies)?',
helpText: 'Nicht-essenzielle Cookies erfordern opt-in Einwilligung', helpText: 'Nicht-essenzielle Cookies erfordern opt-in Einwilligung',
required: true, required: true,
scoreWeights: { risk: 5, complexity: 4, assurance: 6 }, scoreWeights: { risk: 5, complexity: 4, assurance: 6 },
@@ -443,7 +443,7 @@ const BLOCK_6_PRODUCT: ScopeQuestionBlock = {
{ {
id: 'prod_webshop', id: 'prod_webshop',
type: 'boolean', type: 'boolean',
label: 'Betreiben Sie einen Online-Shop?', question: 'Betreiben Sie einen Online-Shop?',
helpText: 'E-Commerce mit Zahlungsabwicklung, Bestellverwaltung', helpText: 'E-Commerce mit Zahlungsabwicklung, Bestellverwaltung',
required: true, required: true,
scoreWeights: { risk: 7, complexity: 6, assurance: 6 }, scoreWeights: { risk: 7, complexity: 6, assurance: 6 },
@@ -451,7 +451,7 @@ const BLOCK_6_PRODUCT: ScopeQuestionBlock = {
{ {
id: 'prod_api_external', id: 'prod_api_external',
type: 'boolean', type: 'boolean',
label: 'Bieten Sie externe APIs an (Daten-Weitergabe an Dritte)?', question: 'Bieten Sie externe APIs an (Daten-Weitergabe an Dritte)?',
helpText: 'Programmierschnittstellen für Partner, Entwickler etc.', helpText: 'Programmierschnittstellen für Partner, Entwickler etc.',
required: true, required: true,
scoreWeights: { risk: 7, complexity: 7, assurance: 7 }, scoreWeights: { risk: 7, complexity: 7, assurance: 7 },
@@ -459,7 +459,7 @@ const BLOCK_6_PRODUCT: ScopeQuestionBlock = {
{ {
id: 'prod_data_broker', id: 'prod_data_broker',
type: 'boolean', type: 'boolean',
label: 'Handeln Sie mit Daten (Data Brokerage, Adresshandel)?', question: 'Handeln Sie mit Daten (Data Brokerage, Adresshandel)?',
helpText: 'Verkauf oder Vermittlung personenbezogener Daten', helpText: 'Verkauf oder Vermittlung personenbezogener Daten',
required: true, required: true,
scoreWeights: { risk: 10, complexity: 8, assurance: 9 }, scoreWeights: { risk: 10, complexity: 8, assurance: 9 },

View File

@@ -42,12 +42,12 @@ export interface ComplianceScores {
* IDs der Fragenblöcke für das Scope-Profiling * IDs der Fragenblöcke für das Scope-Profiling
*/ */
export type ScopeQuestionBlockId = export type ScopeQuestionBlockId =
| 'org_reife' // Organisatorische Reife | 'organisation' // Organisation & Reife
| 'daten_betroffene' // Daten & Betroffene | 'data' // Daten & Betroffene
| 'verarbeitung_zweck' // Verarbeitung & Zweck | 'processing' // Verarbeitung & Zweck
| 'technik_hosting' // Technik & Hosting | 'tech' // Technik & Hosting
| 'rechte_prozesse' // Rechte & Prozesse | 'processes' // Rechte & Prozesse
| 'produktkontext'; // Produktkontext | 'product'; // Produktkontext
/** /**
* Eine einzelne Frage im Scope-Profiling * Eine einzelne Frage im Scope-Profiling
@@ -55,8 +55,6 @@ export type ScopeQuestionBlockId =
export interface ScopeProfilingQuestion { export interface ScopeProfilingQuestion {
/** Eindeutige ID der Frage */ /** Eindeutige ID der Frage */
id: string; id: string;
/** Zugehöriger Block */
block: ScopeQuestionBlockId;
/** Fragetext */ /** Fragetext */
question: string; question: string;
/** Optional: Hilfetext/Erklärung */ /** Optional: Hilfetext/Erklärung */
@@ -103,6 +101,8 @@ export interface ScopeQuestionBlock {
title: string; title: string;
/** Block-Beschreibung */ /** Block-Beschreibung */
description: string; description: string;
/** Reihenfolge des Blocks */
order: number;
/** Fragen in diesem Block */ /** Fragen in diesem Block */
questions: ScopeProfilingQuestion[]; questions: ScopeProfilingQuestion[];
} }