refactor: Admin-Layout komplett entfernt — SDK als einziges Layout
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 32s
CI / test-python-backend-compliance (push) Successful in 31s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 19s
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 32s
CI / test-python-backend-compliance (push) Successful in 31s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 19s
Kaputtes (admin) Layout geloescht (Role-Selection, 404-Sidebar, localhost-Dashboard). SDK-Flow nach /sdk/sdk-flow verschoben. Route-Gruppe (sdk) aufgeloest. Root-Seite redirected auf /sdk. ~25 ungenutzte Dateien/Verzeichnisse entfernt. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
395
admin-compliance/app/sdk/compliance-scope/page.tsx
Normal file
395
admin-compliance/app/sdk/compliance-scope/page.tsx
Normal file
@@ -0,0 +1,395 @@
|
||||
'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 SDK context first (persisted via State API), then localStorage as fallback.
|
||||
// Runs ONCE on mount only — empty deps breaks the dispatch→sdkState→setScopeState→dispatch loop.
|
||||
useEffect(() => {
|
||||
try {
|
||||
// Priority 1: SDK context (loaded from PostgreSQL via State API)
|
||||
const ctxScope = sdkState.complianceScope
|
||||
if (ctxScope && ctxScope.answers?.length > 0) {
|
||||
setScopeState(ctxScope)
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(ctxScope))
|
||||
} else {
|
||||
// Priority 2: localStorage fallback
|
||||
const stored = localStorage.getItem(STORAGE_KEY)
|
||||
if (stored) {
|
||||
const parsed = JSON.parse(stored) as ComplianceScopeState
|
||||
setScopeState(parsed)
|
||||
dispatch({ type: 'SET_COMPLIANCE_SCOPE', payload: parsed })
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load compliance scope state:', error)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
// 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 — pb-28 verhindert dass sticky Footer die unteren Buttons verdeckt */}
|
||||
<div className="p-6 pb-28">
|
||||
{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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user