This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/admin-v2/app/(sdk)/sdk/compliance-scope/page.tsx
Benjamin Admin 9ec5a88af9 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>
2026-02-10 13:42:31 +01:00

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>
)
}