- 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>
387 lines
14 KiB
TypeScript
387 lines
14 KiB
TypeScript
'use client'
|
|
|
|
import React, { useState, useEffect, useCallback, useMemo } from 'react'
|
|
import { useSDK } from '@/lib/sdk'
|
|
import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader/StepHeader'
|
|
import {
|
|
ScopeOverviewTab,
|
|
ScopeWizardTab,
|
|
ScopeDecisionTab,
|
|
ScopeExportTab
|
|
} from '@/components/sdk/compliance-scope'
|
|
import type {
|
|
ComplianceScopeState,
|
|
ScopeProfilingAnswer,
|
|
ScopeDecision
|
|
} from '@/lib/sdk/compliance-scope-types'
|
|
import {
|
|
createEmptyScopeState,
|
|
STORAGE_KEY
|
|
} from '@/lib/sdk/compliance-scope-types'
|
|
import { complianceScopeEngine } from '@/lib/sdk/compliance-scope-engine'
|
|
import { getAllQuestions } from '@/lib/sdk/compliance-scope-profiling'
|
|
|
|
type TabId = 'overview' | 'wizard' | 'decision' | 'export'
|
|
|
|
const TABS: { id: TabId; label: string; icon: string }[] = [
|
|
{ id: 'overview', label: 'Uebersicht', icon: '📊' },
|
|
{ id: 'wizard', label: 'Scope-Profiling', icon: '📋' },
|
|
{ id: 'decision', label: 'Scope-Entscheidung', icon: '⚖️' },
|
|
{ id: 'export', label: 'Export', icon: '📤' },
|
|
]
|
|
|
|
export default function ComplianceScopePage() {
|
|
const { state: sdkState, dispatch } = useSDK()
|
|
|
|
// Active tab state
|
|
const [activeTab, setActiveTab] = useState<TabId>('overview')
|
|
|
|
// Local scope state
|
|
const [scopeState, setScopeState] = useState<ComplianceScopeState>(() => {
|
|
// Try to load from SDK context first
|
|
if (sdkState.complianceScope) {
|
|
return sdkState.complianceScope
|
|
}
|
|
return createEmptyScopeState()
|
|
})
|
|
|
|
// Loading state
|
|
const [isLoading, setIsLoading] = useState(true)
|
|
const [isEvaluating, setIsEvaluating] = useState(false)
|
|
|
|
// Load from localStorage on mount
|
|
useEffect(() => {
|
|
try {
|
|
const stored = localStorage.getItem(STORAGE_KEY)
|
|
if (stored) {
|
|
const parsed = JSON.parse(stored) as ComplianceScopeState
|
|
setScopeState(parsed)
|
|
// Also sync to SDK context
|
|
dispatch({ type: 'SET_COMPLIANCE_SCOPE', payload: parsed })
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load compliance scope state from localStorage:', error)
|
|
} finally {
|
|
setIsLoading(false)
|
|
}
|
|
}, [dispatch])
|
|
|
|
// Save to localStorage and SDK context whenever state changes
|
|
useEffect(() => {
|
|
if (!isLoading) {
|
|
try {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(scopeState))
|
|
dispatch({ type: 'SET_COMPLIANCE_SCOPE', payload: scopeState })
|
|
} catch (error) {
|
|
console.error('Failed to save compliance scope state:', error)
|
|
}
|
|
}
|
|
}, [scopeState, isLoading, dispatch])
|
|
|
|
// Handle answers change from wizard
|
|
const handleAnswersChange = useCallback((answers: ScopeProfilingAnswer[]) => {
|
|
setScopeState(prev => ({
|
|
...prev,
|
|
answers,
|
|
lastModified: new Date().toISOString(),
|
|
}))
|
|
}, [])
|
|
|
|
// Handle evaluate button click
|
|
const handleEvaluate = useCallback(async () => {
|
|
setIsEvaluating(true)
|
|
try {
|
|
// Run the compliance scope engine
|
|
const decision = complianceScopeEngine.evaluate(scopeState.answers)
|
|
|
|
// Update state with decision
|
|
setScopeState(prev => ({
|
|
...prev,
|
|
decision,
|
|
lastModified: new Date().toISOString(),
|
|
}))
|
|
|
|
// Switch to decision tab to show results
|
|
setActiveTab('decision')
|
|
} catch (error) {
|
|
console.error('Failed to evaluate compliance scope:', error)
|
|
// Optionally show error toast/notification
|
|
} finally {
|
|
setIsEvaluating(false)
|
|
}
|
|
}, [scopeState.answers])
|
|
|
|
// Handle start profiling from overview
|
|
const handleStartProfiling = useCallback(() => {
|
|
setActiveTab('wizard')
|
|
}, [])
|
|
|
|
// Handle reset
|
|
const handleReset = useCallback(() => {
|
|
const emptyState = createEmptyScopeState()
|
|
setScopeState(emptyState)
|
|
setActiveTab('overview')
|
|
localStorage.removeItem(STORAGE_KEY)
|
|
}, [])
|
|
|
|
// Calculate completion statistics
|
|
const completionStats = useMemo(() => {
|
|
const allQuestions = getAllQuestions()
|
|
const requiredQuestions = allQuestions.filter(q => q.required)
|
|
const totalQuestions = requiredQuestions.length
|
|
const answeredIds = new Set(scopeState.answers.map(a => a.questionId))
|
|
const answeredQuestions = requiredQuestions.filter(q => answeredIds.has(q.id)).length
|
|
|
|
const completionPercentage = totalQuestions > 0
|
|
? Math.round((answeredQuestions / totalQuestions) * 100)
|
|
: 0
|
|
|
|
const isComplete = answeredQuestions === totalQuestions
|
|
|
|
return {
|
|
total: totalQuestions,
|
|
answered: answeredQuestions,
|
|
percentage: completionPercentage,
|
|
isComplete,
|
|
}
|
|
}, [scopeState.answers])
|
|
|
|
// Auto-enable evaluation when all questions are answered
|
|
const canEvaluate = useMemo(() => {
|
|
return completionStats.isComplete
|
|
}, [completionStats.isComplete])
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="max-w-6xl mx-auto p-6">
|
|
<div className="flex items-center justify-center h-64">
|
|
<div className="text-gray-500">Loading...</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="max-w-6xl mx-auto space-y-6 p-6">
|
|
{/* Step Header */}
|
|
<StepHeader
|
|
stepId="compliance-scope"
|
|
title="Compliance Scope Engine"
|
|
description="Umfang und Tiefe Ihrer Compliance-Dokumentation bestimmen"
|
|
explanation="Die Scope Engine analysiert Ihr Unternehmen anhand von 35 Fragen in 6 Bereichen und bestimmt deterministisch, welche Dokumente in welcher Tiefe benoetigt werden. Das 4-Level-Modell reicht von L1 (Lean Startup) bis L4 (Zertifizierungsbereit). Hard Triggers wie Art. 9 Daten oder Minderjährige heben das Level automatisch an."
|
|
tips={[
|
|
{
|
|
icon: 'lightbulb',
|
|
title: 'Deterministisch',
|
|
description: 'Alle Entscheidungen sind nachvollziehbar — keine KI, keine Black Box. Jede Empfehlung hat eine auditfähige Begründung.'
|
|
},
|
|
{
|
|
icon: 'info',
|
|
title: '4-Level-Modell',
|
|
description: 'L1 (Lean Startup) bis L4 (Zertifizierungsbereit). Das Level bestimmt Dokumentationstiefe und -umfang.'
|
|
},
|
|
{
|
|
icon: 'warning',
|
|
title: 'Hard Triggers',
|
|
description: 'Besondere Datenkategorien (Art. 9), Minderjährige oder Zertifizierungsziele heben das Level automatisch an.'
|
|
},
|
|
]}
|
|
/>
|
|
|
|
{/* Progress Indicator */}
|
|
{completionStats.answered > 0 && (
|
|
<div className="bg-purple-50 border border-purple-200 rounded-lg p-4">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<div className="text-sm font-medium text-purple-900">
|
|
Fortschritt: {completionStats.answered} von {completionStats.total} Fragen beantwortet
|
|
</div>
|
|
<div className="text-sm font-semibold text-purple-700">
|
|
{completionStats.percentage}%
|
|
</div>
|
|
</div>
|
|
<div className="w-full bg-purple-200 rounded-full h-2">
|
|
<div
|
|
className="bg-purple-600 h-2 rounded-full transition-all duration-300"
|
|
style={{ width: `${completionStats.percentage}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Main Content Card */}
|
|
<div className="bg-white rounded-xl border border-gray-200 shadow-sm">
|
|
{/* Tab Navigation */}
|
|
<div className="border-b border-gray-200 px-6">
|
|
<nav className="flex gap-6 -mb-px">
|
|
{TABS.map(tab => (
|
|
<button
|
|
key={tab.id}
|
|
onClick={() => setActiveTab(tab.id)}
|
|
className={`
|
|
flex items-center gap-2 py-4 px-1 border-b-2 font-medium text-sm transition-colors
|
|
${activeTab === tab.id
|
|
? 'border-purple-600 text-purple-700'
|
|
: 'border-transparent text-gray-600 hover:text-gray-800 hover:border-gray-300'
|
|
}
|
|
`}
|
|
>
|
|
<span className="text-lg">{tab.icon}</span>
|
|
<span>{tab.label}</span>
|
|
{tab.id === 'wizard' && completionStats.answered > 0 && (
|
|
<span className="ml-1 px-2 py-0.5 text-xs rounded-full bg-purple-100 text-purple-700">
|
|
{completionStats.percentage}%
|
|
</span>
|
|
)}
|
|
{tab.id === 'decision' && scopeState.decision && (
|
|
<span className="ml-1 w-2 h-2 rounded-full bg-green-500" />
|
|
)}
|
|
</button>
|
|
))}
|
|
</nav>
|
|
</div>
|
|
|
|
{/* Tab Content */}
|
|
<div className="p-6">
|
|
{activeTab === 'overview' && (
|
|
<ScopeOverviewTab
|
|
scopeState={scopeState}
|
|
completionStats={completionStats}
|
|
onStartProfiling={handleStartProfiling}
|
|
onReset={handleReset}
|
|
onGoToWizard={() => setActiveTab('wizard')}
|
|
onGoToDecision={() => setActiveTab('decision')}
|
|
onGoToExport={() => setActiveTab('export')}
|
|
/>
|
|
)}
|
|
|
|
{activeTab === 'wizard' && (
|
|
<ScopeWizardTab
|
|
answers={scopeState.answers}
|
|
onAnswersChange={handleAnswersChange}
|
|
onEvaluate={handleEvaluate}
|
|
canEvaluate={canEvaluate}
|
|
isEvaluating={isEvaluating}
|
|
completionStats={completionStats}
|
|
/>
|
|
)}
|
|
|
|
{activeTab === 'decision' && (
|
|
<ScopeDecisionTab
|
|
decision={scopeState.decision}
|
|
answers={scopeState.answers}
|
|
onBackToWizard={() => setActiveTab('wizard')}
|
|
onGoToExport={() => setActiveTab('export')}
|
|
canEvaluate={canEvaluate}
|
|
onEvaluate={handleEvaluate}
|
|
isEvaluating={isEvaluating}
|
|
/>
|
|
)}
|
|
|
|
{activeTab === 'export' && (
|
|
<ScopeExportTab
|
|
scopeState={scopeState}
|
|
onBackToDecision={() => setActiveTab('decision')}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Quick Action Buttons (Fixed at bottom on mobile) */}
|
|
<div className="sticky bottom-6 flex justify-between items-center gap-4 bg-white rounded-lg border border-gray-200 p-4 shadow-lg">
|
|
<div className="flex items-center gap-3">
|
|
<div className="text-sm text-gray-600">
|
|
{completionStats.isComplete ? (
|
|
<span className="flex items-center gap-2 text-green-700">
|
|
<span className="text-lg">✓</span>
|
|
<span className="font-medium">Profiling abgeschlossen</span>
|
|
</span>
|
|
) : (
|
|
<span className="flex items-center gap-2">
|
|
<span className="text-lg">📋</span>
|
|
<span>
|
|
{completionStats.answered === 0
|
|
? 'Starten Sie mit dem Profiling'
|
|
: `Noch ${completionStats.total - completionStats.answered} Fragen offen`
|
|
}
|
|
</span>
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3">
|
|
{activeTab !== 'wizard' && completionStats.answered > 0 && (
|
|
<button
|
|
onClick={() => setActiveTab('wizard')}
|
|
className="px-4 py-2 text-sm font-medium text-purple-700 bg-purple-50 hover:bg-purple-100 rounded-lg transition-colors"
|
|
>
|
|
Zum Fragebogen
|
|
</button>
|
|
)}
|
|
|
|
{canEvaluate && activeTab !== 'decision' && (
|
|
<button
|
|
onClick={handleEvaluate}
|
|
disabled={isEvaluating}
|
|
className="px-4 py-2 text-sm font-medium text-white bg-purple-600 hover:bg-purple-700 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
{isEvaluating ? 'Evaluiere...' : 'Scope evaluieren'}
|
|
</button>
|
|
)}
|
|
|
|
{scopeState.decision && activeTab !== 'export' && (
|
|
<button
|
|
onClick={() => setActiveTab('export')}
|
|
className="px-4 py-2 text-sm font-medium text-white bg-green-600 hover:bg-green-700 rounded-lg transition-colors"
|
|
>
|
|
Exportieren
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Debug Info (only in development) */}
|
|
{process.env.NODE_ENV === 'development' && (
|
|
<details className="bg-gray-50 border border-gray-200 rounded-lg p-4">
|
|
<summary className="cursor-pointer text-sm font-medium text-gray-700">
|
|
Debug Information
|
|
</summary>
|
|
<div className="mt-3 space-y-2 text-xs font-mono">
|
|
<div>
|
|
<span className="font-semibold">Active Tab:</span> {activeTab}
|
|
</div>
|
|
<div>
|
|
<span className="font-semibold">Total Answers:</span> {scopeState.answers.length}
|
|
</div>
|
|
<div>
|
|
<span className="font-semibold">Answered:</span> {completionStats.answered} ({completionStats.percentage}%)
|
|
</div>
|
|
<div>
|
|
<span className="font-semibold">Has Decision:</span> {scopeState.decision ? 'Yes' : 'No'}
|
|
</div>
|
|
{scopeState.decision && (
|
|
<>
|
|
<div>
|
|
<span className="font-semibold">Level:</span> {scopeState.decision.level}
|
|
</div>
|
|
<div>
|
|
<span className="font-semibold">Score:</span> {scopeState.decision.score}
|
|
</div>
|
|
<div>
|
|
<span className="font-semibold">Hard Triggers:</span> {scopeState.decision.hardTriggers.length}
|
|
</div>
|
|
</>
|
|
)}
|
|
<div>
|
|
<span className="font-semibold">Last Modified:</span> {scopeState.lastModified || 'Never'}
|
|
</div>
|
|
<div>
|
|
<span className="font-semibold">Can Evaluate:</span> {canEvaluate ? 'Yes' : 'No'}
|
|
</div>
|
|
</div>
|
|
</details>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|