Files
breakpilot-compliance/admin-compliance/lib/sdk/context.tsx
Benjamin Admin d3fc4cdaaa
Some checks failed
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) Failing after 45s
CI / test-python-backend-compliance (push) Successful in 35s
CI / test-python-document-crawler (push) Successful in 25s
CI / test-python-dsms-gateway (push) Successful in 21s
feat(sdk): Logo-Navigation, stabile Versionsnummer V001 + Firmenname im Header
- Logo-Klick fuehrt zurueck zur Startseite (Neues/Bestehendes Projekt)
- Neue projectVersion im SDK State (inkrementiert nur bei explizitem Speichern)
- Header zeigt Firmenname + V001-Format statt auto-inkrementierende Sync-Version
- Sidebar Logo von Link auf Button umgestellt mit customerType-Reset

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 08:14:33 +01:00

1161 lines
33 KiB
TypeScript

'use client'
import React, { createContext, useContext, useReducer, useEffect, useCallback, useMemo, useRef } from 'react'
import { useRouter, usePathname } from 'next/navigation'
import {
SDKState,
SDKAction,
SDKStep,
CheckpointStatus,
UseCaseAssessment,
Risk,
Control,
UserPreferences,
CustomerType,
CompanyProfile,
ImportedDocument,
GapAnalysis,
SDKPackageId,
SDK_STEPS,
SDK_PACKAGES,
getStepById,
getStepByUrl,
getNextStep,
getPreviousStep,
getCompletionPercentage,
getPhaseCompletionPercentage,
getPackageCompletionPercentage,
getStepsForPackage,
} from './types'
import { exportToPDF, exportToZIP } from './export'
import { SDKApiClient, getSDKApiClient, resetSDKApiClient } from './api-client'
import { StateSyncManager, createStateSyncManager, SyncState } from './sync'
import { generateDemoState, seedDemoData as seedDemoDataApi, clearDemoData as clearDemoDataApi } from './demo-data'
// =============================================================================
// INITIAL STATE
// =============================================================================
const initialPreferences: UserPreferences = {
language: 'de',
theme: 'light',
compactMode: false,
showHints: true,
autoSave: true,
autoValidate: true,
allowParallelWork: true, // Standard: Paralleles Arbeiten erlaubt
}
const initialState: SDKState = {
// Metadata
version: '1.0.0',
projectVersion: 1,
lastModified: new Date(),
// Tenant & User
tenantId: '',
userId: '',
subscription: 'PROFESSIONAL',
// Customer Type
customerType: null,
// Company Profile
companyProfile: null,
// Compliance Scope
complianceScope: null,
// Source Policy
sourcePolicy: null,
// Progress
currentPhase: 1,
currentStep: 'company-profile',
completedSteps: [],
checkpoints: {},
// Imported Documents (for existing customers)
importedDocuments: [],
gapAnalysis: null,
// Phase 1 Data
useCases: [],
activeUseCase: null,
screening: null,
modules: [],
requirements: [],
controls: [],
evidence: [],
checklist: [],
risks: [],
// Phase 2 Data
aiActClassification: null,
obligations: [],
dsfa: null,
toms: [],
retentionPolicies: [],
vvt: [],
documents: [],
cookieBanner: null,
consents: [],
dsrConfig: null,
escalationWorkflows: [],
// IACE (Industrial AI Compliance Engine)
iaceProjects: [],
// RAG Corpus Versioning
ragCorpusStatus: null,
// Security
sbom: null,
securityIssues: [],
securityBacklog: [],
// Catalog Manager
customCatalogs: {},
// UI State
commandBarHistory: [],
recentSearches: [],
preferences: initialPreferences,
}
// =============================================================================
// EXTENDED ACTION TYPES
// =============================================================================
// Extended action type to include demo data loading
type ExtendedSDKAction =
| SDKAction
| { type: 'LOAD_DEMO_DATA'; payload: Partial<SDKState> }
// =============================================================================
// REDUCER
// =============================================================================
function sdkReducer(state: SDKState, action: ExtendedSDKAction): SDKState {
const updateState = (updates: Partial<SDKState>): SDKState => ({
...state,
...updates,
lastModified: new Date(),
})
switch (action.type) {
case 'SET_STATE':
return updateState(action.payload)
case 'LOAD_DEMO_DATA':
// Load demo data while preserving user preferences
return {
...initialState,
...action.payload,
tenantId: state.tenantId,
userId: state.userId,
preferences: state.preferences,
lastModified: new Date(),
}
case 'SET_CURRENT_STEP': {
const step = getStepById(action.payload)
return updateState({
currentStep: action.payload,
currentPhase: step?.phase || state.currentPhase,
})
}
case 'COMPLETE_STEP':
if (state.completedSteps.includes(action.payload)) {
return state
}
return updateState({
completedSteps: [...state.completedSteps, action.payload],
})
case 'SET_CHECKPOINT_STATUS':
return updateState({
checkpoints: {
...state.checkpoints,
[action.payload.id]: action.payload.status,
},
})
case 'SET_CUSTOMER_TYPE':
return updateState({ customerType: action.payload })
case 'SET_COMPANY_PROFILE':
return updateState({ companyProfile: action.payload })
case 'UPDATE_COMPANY_PROFILE':
return updateState({
companyProfile: state.companyProfile
? { ...state.companyProfile, ...action.payload }
: null,
})
case 'SET_COMPLIANCE_SCOPE':
return updateState({ complianceScope: action.payload })
case 'UPDATE_COMPLIANCE_SCOPE':
return updateState({
complianceScope: state.complianceScope
? { ...state.complianceScope, ...action.payload }
: null,
})
case 'ADD_IMPORTED_DOCUMENT':
return updateState({
importedDocuments: [...state.importedDocuments, action.payload],
})
case 'UPDATE_IMPORTED_DOCUMENT':
return updateState({
importedDocuments: state.importedDocuments.map(doc =>
doc.id === action.payload.id ? { ...doc, ...action.payload.data } : doc
),
})
case 'DELETE_IMPORTED_DOCUMENT':
return updateState({
importedDocuments: state.importedDocuments.filter(doc => doc.id !== action.payload),
})
case 'SET_GAP_ANALYSIS':
return updateState({ gapAnalysis: action.payload })
case 'ADD_USE_CASE':
return updateState({
useCases: [...state.useCases, action.payload],
})
case 'UPDATE_USE_CASE':
return updateState({
useCases: state.useCases.map(uc =>
uc.id === action.payload.id ? { ...uc, ...action.payload.data } : uc
),
})
case 'DELETE_USE_CASE':
return updateState({
useCases: state.useCases.filter(uc => uc.id !== action.payload),
activeUseCase: state.activeUseCase === action.payload ? null : state.activeUseCase,
})
case 'SET_ACTIVE_USE_CASE':
return updateState({ activeUseCase: action.payload })
case 'SET_SCREENING':
return updateState({ screening: action.payload })
case 'ADD_MODULE':
return updateState({
modules: [...state.modules, action.payload],
})
case 'UPDATE_MODULE':
return updateState({
modules: state.modules.map(m =>
m.id === action.payload.id ? { ...m, ...action.payload.data } : m
),
})
case 'ADD_REQUIREMENT':
return updateState({
requirements: [...state.requirements, action.payload],
})
case 'UPDATE_REQUIREMENT':
return updateState({
requirements: state.requirements.map(r =>
r.id === action.payload.id ? { ...r, ...action.payload.data } : r
),
})
case 'ADD_CONTROL':
return updateState({
controls: [...state.controls, action.payload],
})
case 'UPDATE_CONTROL':
return updateState({
controls: state.controls.map(c =>
c.id === action.payload.id ? { ...c, ...action.payload.data } : c
),
})
case 'ADD_EVIDENCE':
return updateState({
evidence: [...state.evidence, action.payload],
})
case 'UPDATE_EVIDENCE':
return updateState({
evidence: state.evidence.map(e =>
e.id === action.payload.id ? { ...e, ...action.payload.data } : e
),
})
case 'DELETE_EVIDENCE':
return updateState({
evidence: state.evidence.filter(e => e.id !== action.payload),
})
case 'ADD_RISK':
return updateState({
risks: [...state.risks, action.payload],
})
case 'UPDATE_RISK':
return updateState({
risks: state.risks.map(r =>
r.id === action.payload.id ? { ...r, ...action.payload.data } : r
),
})
case 'DELETE_RISK':
return updateState({
risks: state.risks.filter(r => r.id !== action.payload),
})
case 'SET_AI_ACT_RESULT':
return updateState({ aiActClassification: action.payload })
case 'ADD_OBLIGATION':
return updateState({
obligations: [...state.obligations, action.payload],
})
case 'UPDATE_OBLIGATION':
return updateState({
obligations: state.obligations.map(o =>
o.id === action.payload.id ? { ...o, ...action.payload.data } : o
),
})
case 'SET_DSFA':
return updateState({ dsfa: action.payload })
case 'ADD_TOM':
return updateState({
toms: [...state.toms, action.payload],
})
case 'UPDATE_TOM':
return updateState({
toms: state.toms.map(t =>
t.id === action.payload.id ? { ...t, ...action.payload.data } : t
),
})
case 'ADD_RETENTION_POLICY':
return updateState({
retentionPolicies: [...state.retentionPolicies, action.payload],
})
case 'UPDATE_RETENTION_POLICY':
return updateState({
retentionPolicies: state.retentionPolicies.map(p =>
p.id === action.payload.id ? { ...p, ...action.payload.data } : p
),
})
case 'ADD_PROCESSING_ACTIVITY':
return updateState({
vvt: [...state.vvt, action.payload],
})
case 'UPDATE_PROCESSING_ACTIVITY':
return updateState({
vvt: state.vvt.map(p =>
p.id === action.payload.id ? { ...p, ...action.payload.data } : p
),
})
case 'ADD_DOCUMENT':
return updateState({
documents: [...state.documents, action.payload],
})
case 'UPDATE_DOCUMENT':
return updateState({
documents: state.documents.map(d =>
d.id === action.payload.id ? { ...d, ...action.payload.data } : d
),
})
case 'SET_COOKIE_BANNER':
return updateState({ cookieBanner: action.payload })
case 'SET_DSR_CONFIG':
return updateState({ dsrConfig: action.payload })
case 'ADD_ESCALATION_WORKFLOW':
return updateState({
escalationWorkflows: [...state.escalationWorkflows, action.payload],
})
case 'UPDATE_ESCALATION_WORKFLOW':
return updateState({
escalationWorkflows: state.escalationWorkflows.map(w =>
w.id === action.payload.id ? { ...w, ...action.payload.data } : w
),
})
case 'ADD_SECURITY_ISSUE':
return updateState({
securityIssues: [...state.securityIssues, action.payload],
})
case 'UPDATE_SECURITY_ISSUE':
return updateState({
securityIssues: state.securityIssues.map(i =>
i.id === action.payload.id ? { ...i, ...action.payload.data } : i
),
})
case 'ADD_BACKLOG_ITEM':
return updateState({
securityBacklog: [...state.securityBacklog, action.payload],
})
case 'UPDATE_BACKLOG_ITEM':
return updateState({
securityBacklog: state.securityBacklog.map(i =>
i.id === action.payload.id ? { ...i, ...action.payload.data } : i
),
})
case 'ADD_COMMAND_HISTORY':
return updateState({
commandBarHistory: [action.payload, ...state.commandBarHistory].slice(0, 50),
})
case 'SET_PREFERENCES':
return updateState({
preferences: { ...state.preferences, ...action.payload },
})
case 'ADD_CUSTOM_CATALOG_ENTRY': {
const entry = action.payload
const existing = state.customCatalogs[entry.catalogId] || []
return updateState({
customCatalogs: {
...state.customCatalogs,
[entry.catalogId]: [...existing, entry],
},
})
}
case 'UPDATE_CUSTOM_CATALOG_ENTRY': {
const { catalogId, entryId, data } = action.payload
const entries = state.customCatalogs[catalogId] || []
return updateState({
customCatalogs: {
...state.customCatalogs,
[catalogId]: entries.map(e =>
e.id === entryId ? { ...e, data: { ...e.data, ...data }, updatedAt: new Date().toISOString() } : e
),
},
})
}
case 'DELETE_CUSTOM_CATALOG_ENTRY': {
const { catalogId, entryId } = action.payload
const items = state.customCatalogs[catalogId] || []
return updateState({
customCatalogs: {
...state.customCatalogs,
[catalogId]: items.filter(e => e.id !== entryId),
},
})
}
case 'RESET_STATE':
return { ...initialState, lastModified: new Date() }
default:
return state
}
}
// =============================================================================
// CONTEXT TYPES
// =============================================================================
interface SDKContextValue {
state: SDKState
dispatch: React.Dispatch<ExtendedSDKAction>
// Navigation
currentStep: SDKStep | undefined
goToStep: (stepId: string) => void
goToNextStep: () => void
goToPreviousStep: () => void
canGoNext: boolean
canGoPrevious: boolean
// Progress
completionPercentage: number
phase1Completion: number
phase2Completion: number
packageCompletion: Record<SDKPackageId, number>
// Customer Type
setCustomerType: (type: CustomerType) => void
// Company Profile
setCompanyProfile: (profile: CompanyProfile) => void
updateCompanyProfile: (updates: Partial<CompanyProfile>) => void
// Compliance Scope
setComplianceScope: (scope: import('./compliance-scope-types').ComplianceScopeState) => void
updateComplianceScope: (updates: Partial<import('./compliance-scope-types').ComplianceScopeState>) => void
// Import (for existing customers)
addImportedDocument: (doc: ImportedDocument) => void
setGapAnalysis: (analysis: GapAnalysis) => void
// Checkpoints
validateCheckpoint: (checkpointId: string) => Promise<CheckpointStatus>
overrideCheckpoint: (checkpointId: string, reason: string) => Promise<void>
getCheckpointStatus: (checkpointId: string) => CheckpointStatus | undefined
// State Updates
updateUseCase: (id: string, data: Partial<UseCaseAssessment>) => void
addRisk: (risk: Risk) => void
updateControl: (id: string, data: Partial<Control>) => void
// Persistence
saveState: () => Promise<void>
loadState: () => Promise<void>
// Demo Data
loadDemoData: (demoState: Partial<SDKState>) => void
seedDemoData: () => Promise<{ success: boolean; message: string }>
clearDemoData: () => Promise<boolean>
isDemoDataLoaded: boolean
// Sync
syncState: SyncState
forceSyncToServer: () => Promise<void>
isOnline: boolean
// Export
exportState: (format: 'json' | 'pdf' | 'zip') => Promise<Blob>
// Command Bar
isCommandBarOpen: boolean
setCommandBarOpen: (open: boolean) => void
}
const SDKContext = createContext<SDKContextValue | null>(null)
// =============================================================================
// PROVIDER
// =============================================================================
const SDK_STORAGE_KEY = 'ai-compliance-sdk-state'
interface SDKProviderProps {
children: React.ReactNode
tenantId?: string
userId?: string
enableBackendSync?: boolean
}
export function SDKProvider({
children,
tenantId = 'default',
userId = 'default',
enableBackendSync = false,
}: SDKProviderProps) {
const router = useRouter()
const pathname = usePathname()
const [state, dispatch] = useReducer(sdkReducer, {
...initialState,
tenantId,
userId,
})
const [isCommandBarOpen, setCommandBarOpen] = React.useState(false)
const [isInitialized, setIsInitialized] = React.useState(false)
const [syncState, setSyncState] = React.useState<SyncState>({
status: 'idle',
lastSyncedAt: null,
localVersion: 0,
serverVersion: 0,
pendingChanges: 0,
error: null,
})
const [isOnline, setIsOnline] = React.useState(true)
// Refs for sync manager and API client
const apiClientRef = useRef<SDKApiClient | null>(null)
const syncManagerRef = useRef<StateSyncManager | null>(null)
// Initialize API client and sync manager
useEffect(() => {
if (enableBackendSync && typeof window !== 'undefined') {
apiClientRef.current = getSDKApiClient(tenantId)
syncManagerRef.current = createStateSyncManager(
apiClientRef.current,
tenantId,
{
debounceMs: 2000,
maxRetries: 3,
},
{
onSyncStart: () => {
setSyncState(prev => ({ ...prev, status: 'syncing' }))
},
onSyncComplete: (syncedState) => {
setSyncState(prev => ({
...prev,
status: 'idle',
lastSyncedAt: new Date(),
pendingChanges: 0,
}))
// Update state if it differs from current
if (syncedState.lastModified > state.lastModified) {
dispatch({ type: 'SET_STATE', payload: syncedState })
}
},
onSyncError: (error) => {
setSyncState(prev => ({
...prev,
status: 'error',
error: error.message,
}))
},
onConflict: () => {
setSyncState(prev => ({ ...prev, status: 'conflict' }))
},
onOffline: () => {
setIsOnline(false)
setSyncState(prev => ({ ...prev, status: 'offline' }))
},
onOnline: () => {
setIsOnline(true)
setSyncState(prev => ({ ...prev, status: 'idle' }))
},
}
)
}
return () => {
if (syncManagerRef.current) {
syncManagerRef.current.destroy()
syncManagerRef.current = null
}
if (enableBackendSync) {
resetSDKApiClient()
apiClientRef.current = null
}
}
}, [enableBackendSync, tenantId])
// Sync current step with URL
useEffect(() => {
if (pathname) {
const step = getStepByUrl(pathname)
if (step && step.id !== state.currentStep) {
dispatch({ type: 'SET_CURRENT_STEP', payload: step.id })
}
}
}, [pathname, state.currentStep])
// Load state on mount (localStorage first, then server)
useEffect(() => {
const loadInitialState = async () => {
try {
// First, try loading from localStorage
const stored = localStorage.getItem(`${SDK_STORAGE_KEY}-${tenantId}`)
if (stored) {
const parsed = JSON.parse(stored)
if (parsed.lastModified) {
parsed.lastModified = new Date(parsed.lastModified)
}
dispatch({ type: 'SET_STATE', payload: parsed })
}
// Then, try loading from server if backend sync is enabled
if (enableBackendSync && syncManagerRef.current) {
const serverState = await syncManagerRef.current.loadFromServer()
if (serverState) {
// Server state is newer, use it
const localTime = stored ? new Date(JSON.parse(stored).lastModified).getTime() : 0
const serverTime = new Date(serverState.lastModified).getTime()
if (serverTime > localTime) {
dispatch({ type: 'SET_STATE', payload: serverState })
}
}
}
} catch (error) {
console.error('Failed to load SDK state:', error)
}
setIsInitialized(true)
}
loadInitialState()
}, [tenantId, enableBackendSync])
// Auto-save to localStorage and sync to server
useEffect(() => {
if (!isInitialized || !state.preferences.autoSave) return
const saveTimeout = setTimeout(() => {
try {
// Save to localStorage
localStorage.setItem(`${SDK_STORAGE_KEY}-${tenantId}`, JSON.stringify(state))
// Sync to server if backend sync is enabled
if (enableBackendSync && syncManagerRef.current) {
syncManagerRef.current.queueSync(state)
}
} catch (error) {
console.error('Failed to save SDK state:', error)
}
}, 1000)
return () => clearTimeout(saveTimeout)
}, [state, tenantId, isInitialized, enableBackendSync])
// Keyboard shortcut for Command Bar
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault()
setCommandBarOpen(prev => !prev)
}
if (e.key === 'Escape' && isCommandBarOpen) {
setCommandBarOpen(false)
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [isCommandBarOpen])
// Navigation
const currentStep = useMemo(() => getStepById(state.currentStep), [state.currentStep])
const goToStep = useCallback(
(stepId: string) => {
const step = getStepById(stepId)
if (step) {
dispatch({ type: 'SET_CURRENT_STEP', payload: stepId })
router.push(step.url)
}
},
[router]
)
const goToNextStep = useCallback(() => {
const nextStep = getNextStep(state.currentStep, state)
if (nextStep) {
goToStep(nextStep.id)
}
}, [state, goToStep])
const goToPreviousStep = useCallback(() => {
const prevStep = getPreviousStep(state.currentStep, state)
if (prevStep) {
goToStep(prevStep.id)
}
}, [state, goToStep])
const canGoNext = useMemo(() => {
return getNextStep(state.currentStep, state) !== undefined
}, [state])
const canGoPrevious = useMemo(() => {
return getPreviousStep(state.currentStep, state) !== undefined
}, [state])
// Progress
const completionPercentage = useMemo(() => getCompletionPercentage(state), [state])
const phase1Completion = useMemo(() => getPhaseCompletionPercentage(state, 1), [state])
const phase2Completion = useMemo(() => getPhaseCompletionPercentage(state, 2), [state])
// Package Completion
const packageCompletion = useMemo(() => {
const completion: Record<SDKPackageId, number> = {
'vorbereitung': getPackageCompletionPercentage(state, 'vorbereitung'),
'analyse': getPackageCompletionPercentage(state, 'analyse'),
'dokumentation': getPackageCompletionPercentage(state, 'dokumentation'),
'rechtliche-texte': getPackageCompletionPercentage(state, 'rechtliche-texte'),
'betrieb': getPackageCompletionPercentage(state, 'betrieb'),
}
return completion
}, [state])
// Customer Type
const setCustomerType = useCallback((type: CustomerType) => {
dispatch({ type: 'SET_CUSTOMER_TYPE', payload: type })
}, [])
// Company Profile
const setCompanyProfile = useCallback((profile: CompanyProfile) => {
dispatch({ type: 'SET_COMPANY_PROFILE', payload: profile })
}, [])
const updateCompanyProfile = useCallback((updates: Partial<CompanyProfile>) => {
dispatch({ type: 'UPDATE_COMPANY_PROFILE', payload: updates })
}, [])
// Compliance Scope
const setComplianceScope = useCallback((scope: import('./compliance-scope-types').ComplianceScopeState) => {
dispatch({ type: 'SET_COMPLIANCE_SCOPE', payload: scope })
}, [])
const updateComplianceScope = useCallback((updates: Partial<import('./compliance-scope-types').ComplianceScopeState>) => {
dispatch({ type: 'UPDATE_COMPLIANCE_SCOPE', payload: updates })
}, [])
// Import Document
const addImportedDocument = useCallback((doc: ImportedDocument) => {
dispatch({ type: 'ADD_IMPORTED_DOCUMENT', payload: doc })
}, [])
// Gap Analysis
const setGapAnalysis = useCallback((analysis: GapAnalysis) => {
dispatch({ type: 'SET_GAP_ANALYSIS', payload: analysis })
}, [])
// Checkpoints
const validateCheckpoint = useCallback(
async (checkpointId: string): Promise<CheckpointStatus> => {
// Try backend validation if available
if (enableBackendSync && apiClientRef.current) {
try {
const result = await apiClientRef.current.validateCheckpoint(checkpointId, state)
const status: CheckpointStatus = {
checkpointId: result.checkpointId,
passed: result.passed,
validatedAt: new Date(result.validatedAt),
validatedBy: result.validatedBy,
errors: result.errors,
warnings: result.warnings,
}
dispatch({ type: 'SET_CHECKPOINT_STATUS', payload: { id: checkpointId, status } })
return status
} catch {
// Fall back to local validation
}
}
// Local validation
const status: CheckpointStatus = {
checkpointId,
passed: true,
validatedAt: new Date(),
validatedBy: 'SYSTEM',
errors: [],
warnings: [],
}
switch (checkpointId) {
case 'CP-PROF':
if (!state.companyProfile || !state.companyProfile.isComplete) {
status.passed = false
status.errors.push({
ruleId: 'prof-complete',
field: 'companyProfile',
message: 'Unternehmensprofil muss vollständig ausgefüllt werden',
severity: 'ERROR',
})
}
break
case 'CP-UC':
if (state.useCases.length === 0) {
status.passed = false
status.errors.push({
ruleId: 'uc-min-count',
field: 'useCases',
message: 'Mindestens ein Anwendungsfall muss erstellt werden',
severity: 'ERROR',
})
}
break
case 'CP-SCAN':
if (!state.screening || state.screening.status !== 'COMPLETED') {
status.passed = false
status.errors.push({
ruleId: 'scan-complete',
field: 'screening',
message: 'Security Scan muss abgeschlossen sein',
severity: 'ERROR',
})
}
break
case 'CP-MOD':
if (state.modules.length === 0) {
status.passed = false
status.errors.push({
ruleId: 'mod-min-count',
field: 'modules',
message: 'Mindestens ein Modul muss zugewiesen werden',
severity: 'ERROR',
})
}
break
case 'CP-RISK':
const criticalRisks = state.risks.filter(
r => r.severity === 'CRITICAL' || r.severity === 'HIGH'
)
const unmitigatedRisks = criticalRisks.filter(
r => r.mitigation.length === 0
)
if (unmitigatedRisks.length > 0) {
status.passed = false
status.errors.push({
ruleId: 'critical-risks-mitigated',
field: 'risks',
message: `${unmitigatedRisks.length} kritische Risiken ohne Mitigationsmaßnahmen`,
severity: 'ERROR',
})
}
break
}
dispatch({ type: 'SET_CHECKPOINT_STATUS', payload: { id: checkpointId, status } })
return status
},
[state, enableBackendSync]
)
const overrideCheckpoint = useCallback(
async (checkpointId: string, reason: string): Promise<void> => {
const existingStatus = state.checkpoints[checkpointId]
const overriddenStatus: CheckpointStatus = {
...existingStatus,
checkpointId,
passed: true,
overrideReason: reason,
overriddenBy: state.userId,
overriddenAt: new Date(),
errors: [],
warnings: existingStatus?.warnings || [],
}
dispatch({ type: 'SET_CHECKPOINT_STATUS', payload: { id: checkpointId, status: overriddenStatus } })
},
[state.checkpoints, state.userId]
)
const getCheckpointStatus = useCallback(
(checkpointId: string): CheckpointStatus | undefined => {
return state.checkpoints[checkpointId]
},
[state.checkpoints]
)
// State Updates
const updateUseCase = useCallback(
(id: string, data: Partial<UseCaseAssessment>) => {
dispatch({ type: 'UPDATE_USE_CASE', payload: { id, data } })
},
[]
)
const addRisk = useCallback((risk: Risk) => {
dispatch({ type: 'ADD_RISK', payload: risk })
}, [])
const updateControl = useCallback(
(id: string, data: Partial<Control>) => {
dispatch({ type: 'UPDATE_CONTROL', payload: { id, data } })
},
[]
)
// Demo Data Loading
const loadDemoData = useCallback((demoState: Partial<SDKState>) => {
dispatch({ type: 'LOAD_DEMO_DATA', payload: demoState })
}, [])
// Seed demo data via API (stores like real customer data)
const seedDemoData = useCallback(async (): Promise<{ success: boolean; message: string }> => {
try {
// Generate demo state
const demoState = generateDemoState(tenantId, userId) as SDKState
// Save via API (same path as real customer data)
if (enableBackendSync && apiClientRef.current) {
await apiClientRef.current.saveState(demoState)
}
// Also save to localStorage for immediate availability
localStorage.setItem(`${SDK_STORAGE_KEY}-${tenantId}`, JSON.stringify(demoState))
// Update local state
dispatch({ type: 'LOAD_DEMO_DATA', payload: demoState })
return { success: true, message: `Demo-Daten erfolgreich geladen für Tenant ${tenantId}` }
} catch (error) {
console.error('Failed to seed demo data:', error)
return {
success: false,
message: error instanceof Error ? error.message : 'Unbekannter Fehler beim Laden der Demo-Daten',
}
}
}, [tenantId, userId, enableBackendSync])
// Clear demo data
const clearDemoData = useCallback(async (): Promise<boolean> => {
try {
// Delete from API
if (enableBackendSync && apiClientRef.current) {
await apiClientRef.current.deleteState()
}
// Clear localStorage
localStorage.removeItem(`${SDK_STORAGE_KEY}-${tenantId}`)
// Reset local state
dispatch({ type: 'RESET_STATE' })
return true
} catch (error) {
console.error('Failed to clear demo data:', error)
return false
}
}, [tenantId, enableBackendSync])
// Check if demo data is loaded (has use cases with demo- prefix)
const isDemoDataLoaded = useMemo(() => {
return state.useCases.length > 0 && state.useCases.some(uc => uc.id.startsWith('demo-'))
}, [state.useCases])
// Persistence
const saveState = useCallback(async (): Promise<void> => {
try {
localStorage.setItem(`${SDK_STORAGE_KEY}-${tenantId}`, JSON.stringify(state))
if (enableBackendSync && syncManagerRef.current) {
await syncManagerRef.current.forcSync(state)
}
} catch (error) {
console.error('Failed to save SDK state:', error)
throw error
}
}, [state, tenantId, enableBackendSync])
const loadState = useCallback(async (): Promise<void> => {
try {
if (enableBackendSync && syncManagerRef.current) {
const serverState = await syncManagerRef.current.loadFromServer()
if (serverState) {
dispatch({ type: 'SET_STATE', payload: serverState })
return
}
}
// Fall back to localStorage
const stored = localStorage.getItem(`${SDK_STORAGE_KEY}-${tenantId}`)
if (stored) {
const parsed = JSON.parse(stored)
dispatch({ type: 'SET_STATE', payload: parsed })
}
} catch (error) {
console.error('Failed to load SDK state:', error)
throw error
}
}, [tenantId, enableBackendSync])
// Force sync to server
const forceSyncToServer = useCallback(async (): Promise<void> => {
if (enableBackendSync && syncManagerRef.current) {
await syncManagerRef.current.forcSync(state)
}
}, [state, enableBackendSync])
// Export
const exportState = useCallback(
async (format: 'json' | 'pdf' | 'zip'): Promise<Blob> => {
switch (format) {
case 'json':
return new Blob([JSON.stringify(state, null, 2)], {
type: 'application/json',
})
case 'pdf':
return exportToPDF(state)
case 'zip':
return exportToZIP(state)
default:
throw new Error(`Unknown export format: ${format}`)
}
},
[state]
)
const value: SDKContextValue = {
state,
dispatch,
currentStep,
goToStep,
goToNextStep,
goToPreviousStep,
canGoNext,
canGoPrevious,
completionPercentage,
phase1Completion,
phase2Completion,
packageCompletion,
setCustomerType,
setCompanyProfile,
updateCompanyProfile,
setComplianceScope,
updateComplianceScope,
addImportedDocument,
setGapAnalysis,
validateCheckpoint,
overrideCheckpoint,
getCheckpointStatus,
updateUseCase,
addRisk,
updateControl,
saveState,
loadState,
loadDemoData,
seedDemoData,
clearDemoData,
isDemoDataLoaded,
syncState,
forceSyncToServer,
isOnline,
exportState,
isCommandBarOpen,
setCommandBarOpen,
}
return <SDKContext.Provider value={value}>{children}</SDKContext.Provider>
}
// =============================================================================
// HOOK
// =============================================================================
export function useSDK(): SDKContextValue {
const context = useContext(SDKContext)
if (!context) {
throw new Error('useSDK must be used within SDKProvider')
}
return context
}
// =============================================================================
// EXPORTS
// =============================================================================
export { SDKContext, initialState }